编写软件很容易——至少对于全新开发的软件来说如此。但修改其他开发人员编写的代码或自己六个月前编写的代码,往好了说是无聊透顶,往坏了说是噩梦。软件可以运行,但你却不确定具体是怎么运行的。它包含了所有正确的框架和模式,并且采用了敏捷方法开发,但向代码库中引入新功能却比预想的要难得多。即使是业务专家也帮不上忙,因为代码与他们使用的语言毫无相似之处。在这样的系统上工作变成了一件苦差事,让开发人员感到沮丧,也失去了编码的乐趣。
WRITING SOFTWARE IS EASY— at least if it’s greenfield software. When it comes to modifying code written by other developers or code you wrote six months ago, it can be a bit of a bore at best and a nightmare at worst. The software works, but you aren’t sure exactly how. It contains all the right frameworks and patterns, and has been created using an agile approach, but introducing new features into the codebase is harder than it should be. Even business experts aren’t helpful because the code bears no resemblance to the language they use. Working on such systems becomes a chore, leaving developers frustrated and devoid of any coding pleasure.
领域驱动设计 (DDD) 是一个将代码与问题领域的实际情况相结合的过程。随着产品的不断发展,添加新功能变得像过去那种全新开发模式一样轻松。尽管 DDD 理解软件模式、原则、方法和框架的必要性,但它更重视开发人员和领域专家的合作,以平等地理解领域概念、策略和逻辑。凭借对问题领域的更深入了解以及与业务部门的协同作用,开发人员更有可能构建出更具可读性且更易于适应未来增强的软件。
Domain-Driven Design (DDD) is a process that aligns your code with the reality of your problem domain. As your product evolves, adding new features becomes as easy as it was in the good old days of greenfield development. Although DDD understands the need for software patterns, principles, methodologies, and frameworks, it values developers and domain experts working together to understand domain concepts, policies, and logic equally. With a greater knowledge of the problem domain and a synergy with the business, developers are more likely to build software that is more readable and easier to adapt for future enhancement.
遵循 DDD 理念将赋予开发人员有效处理大型或复杂业务系统所需的知识和技能。未来的增强请求将不再令人畏惧,开发人员也不再对遗留应用程序抱有偏见。事实上,“遗留”一词在开发人员的心中将被重新定义:一个能够持续为业务创造价值的系统。
Following the DDD philosophy will give developers the knowledge and skills they need to tackle large or complex business systems effectively. Future enhancement requests won’t be met with an air of dread, and developers will no longer have stigma attached to the legacy application. In fact, the term legacy will be recategorized in a developer’s mind as meaning this: a system that continues to give value for the business.
本书提供了有关如何在您自己的项目中应用 DDD 模式和实践的透彻理解,但在深入研究细节之前,最好先从整体上了解一下该哲学,这样您就可以真正了解 DDD 的含义。
This book provides a thorough understanding of how you can apply the patterns and practices of DDD on your own projects, but before delving into the details, it’s good to take a bird’s-eye view of the philosophy so you can get a sense of what DDD is really all about.
在开发解决方案之前,您必须先了解问题所在。DDD 强调需要关注业务问题领域:其术语、软件开发背后的核心原因以及成功对业务的意义。开发团队需要像重视技术专长一样重视领域知识,这对于深入了解问题领域并将大型领域分解为更小的子领域至关重要。
Before you can develop a solution, you must understand the problem. DDD emphasizes the need to focus on the business problem domain: its terminology, the core reasons behind why the software is being developed, and what success means to the business. The need for the development team to value domain knowledge just as much as technical expertise is vital to gain a deeper insight into the problem domain and to decompose large domains into smaller subdomains.
图 I.1展示了本书第一部分将要介绍的 DDD 问题空间的高层概述。
Figure I.1 shows a high-level overview of the problem space of DDD that will be introduced in the first part of this book.
图 I.1 DDD 问题空间的蓝图。
FIGURE I.1 A blueprint of the problem space of DDD.
当你对问题领域有深入的理解时,DDD 的战略模式可以帮助你实现与问题空间协同的技术解决方案。模式可以保护系统中对产品成功至关重要的核心部分,使其免受通用领域的干扰。隔离集成组件,使得修改这些组件时不会对整个系统产生连锁反应。
When you have a sound understanding of the problem domain, strategic patterns of DDD can help you implement a technical solution in synergy with the problem space. Patterns enable core parts of your system that are crucial to the success of the product to be protected from the generic areas. Isolating integral components allows them to be modified without having a rippling effect throughout the system.
产品中足够复杂或频繁更改的核心部分应该基于模型。DDD 的战术模式以及模型驱动设计将帮助您在代码中创建实用的领域模型。模型是所有领域逻辑的所在地,这些逻辑使您的应用程序能够满足业务用例。模型与技术复杂性相分离,以便业务规则和策略能够不断发展。与问题领域协同的模型将使您的软件具有适应性,并易于其他开发人员和业务专家理解。
Core parts of your product that are sufficiently complex or will frequently change should be based on a model. The tactical patterns of DDD along with Model-Driven Design will help you create a useful model of your domain in code. A model is the home to all of the domain logic that enables your application to fulfill business use cases. A model is kept separate from technical complexities to enable business rules and policies to evolve. A model that is in synergy with the problem domain will enable your software to be adaptable and understood by other developers and business experts.
图 I.2展示了本书第一部分介绍的 DDD 解决方案空间的高级概述。
Figure I.2 shows a high-level overview of the solution space of DDD that is introduced in the first part of this book.
图 I.2领域驱动设计解决方案空间蓝图。
FIGURE I.2 A blueprint of the solution space of Domain-Driven Design.
本书分为四个部分。第一部分重点介绍领域驱动设计 (DDD) 的理念、原则和实践。第二部分详细介绍了集成有界上下文的战略模式。第三部分涵盖了创建有效领域模型的战术模式。第四部分深入探讨了可用于利用领域模型并构建有效应用程序的设计模式。
This book is divided into four parts. Part I focuses on the philosophy, principles, and practices of DDD. Part II details the strategic patterns of integrating bounded contexts. Part III covers tactical patterns for creating effective domain models. Part IV delves into design patterns you can apply to utilize the domain model and build effective applications.
第一部分向您介绍 DDD 的原理和实践。
Part I introduces you to the principles and practices of DDD.
DDD 是一种帮助应对复杂领域软件构建挑战的理念。本章将介绍这一理念,并解释为什么语言、协作和上下文是 DDD 最重要的方面,以及为什么 DDD 不仅仅是一堆编码模式的集合。
DDD is a philosophy to help with the challenges of building software for complex domains. This chapter introduces the philosophy and explains why language, collaboration, and context are the most important facets of DDD and why it is much more than a collection of coding patterns.
理解复杂的问题领域对于创建可维护的软件至关重要。与领域专家进行知识交流是解锁这些知识的关键。第二章详细介绍了使开发团队能够与领域专家协作、实验和学习以创建有效领域模型的技术。
Making sense of a complex problem domain is essential to creating maintainable software. Knowledge crunching with domain experts is key to unlocking that knowledge. Chapter 2 details techniques to enable development teams to collaborate, experiment, and learn with domain experts to create an effective domain model.
第三章讲解了如何提炼大型问题域并识别出问题中最重要的部分:核心域。此外,它还解释了为什么应该将时间和精力集中在核心域上,并将其与不太重要的支持域和通用域隔离开来。
Chapter 3 explains how to distill large problem domains and identify the most important part of a problem: the core domain. It then explains why you should focus time and energy in the core domain and isolate it from the less important supporting and generic domains.
业务同事会根据您所处理的问题领域理解一个分析模型。开发团队拥有该模型的各自代码版本。为了便于业务团队和技术团队协作,需要使用统一的模型。一种通用语言和对问题空间的共同理解,是将分析模型与代码模型绑定在一起的关键。共享语言的概念是领域驱动设计 (DDD) 的核心,也是其哲学的基石。一种由开发团队和业务专家共同创建的、用于描述领域术语和概念的语言,对于促进复杂系统上的沟通至关重要。
Business colleagues understand an analysis model based on the problem area you are working within. Development teams have their own code version of this model. In order for business and technical teams to collaborate a single model is needed. A ubiquitous language and a shared understanding of the problem space is what binds the analysis model to the code model. The idea of a shared language is core to DDD and underpins the philosophy. A language describing the terms and concepts of the domain, which is created by both the development team and the business experts, is vital to aid communication on complex systems.
第五章扩展了领域模型在应用程序中的角色及其承担的职责。本章还介绍了可用于实现领域模型的各种模式以及它们最适合的场景。
Chapter 5 expands on the role of the domain model within your application and the responsibilities it takes on. The chapter also presents the various patterns that can be used to implement a domain model and what situations they are most appropriate for.
在大型解决方案中,可能存在多个模型。保护每个模型的完整性至关重要,这样才能消除语言歧义和概念被不同团队不当重复使用的可能性。一种名为“有界上下文”的战略模式旨在隔离和保护上下文中的模型,同时确保其能够与其他模型协作。
In large solutions more than a single model may exist. It is important to protect the integrity of each model to remove the chance of ambiguity in the language and concepts being reused inappropriately by different teams. The strategic pattern known as bounded context is designed to isolate and protect a model in a context while ensuring it can collaborate with other models.
使用上下文图来理解应用程序中不同模型之间的关系以及它们如何集成,对于战略设计至关重要。上下文图不仅涵盖技术集成,还涵盖团队之间的政治关系。上下文图提供了全局视图,可以帮助团队在整个全局背景下理解他们的模型。
Using a context map to understand the relationships between different models in an application and how they integrate is vital for strategic design. It is not only the technical integrations that context maps cover but also the political relationships between teams. Context maps provide a view of the landscape that can help teams understand their model in the context of the entire landscape.
应用程序需要能够利用领域模型来满足业务用例。第 8 章介绍了一些架构模式,用于构建应用程序以保持领域模型的完整性。
An application needs to be able to utilize the domain model to satisfy business use cases. Chapter 8 introduces architectural patterns to structure your applications to retain the integrity of your domain model.
第九章介绍了团队在应用 DDD 时面临的常见问题,以及了解何时不使用它的重要性。本章还重点阐述了为什么将 DDD 应用于简单问题会导致系统过度设计和不必要的复杂性。
Chapter 9 describes the common issues teams face when applying DDD and why it’s important to know when not to use it. The chapter also focuses on why applying DDD to simple problems can lead to overdesigned systems and needless complexity.
第 10 章介绍了如何推广 DDD,以及如何将这些原则和实践应用到项目中。它解释了为什么探索和实验比试图创建完美的领域模型更有助于构建优秀的软件。
Chapter 10 covers techniques to sell DDD and to start applying the principles and practices to your projects. It explains how exploration and experimentation are more useful to build great software than trying to create the perfect domain model.
第二部分将向您展示如何集成有界上下文,并详细介绍了构建有界上下文的各种选项。此外,还将提供代码示例,详细说明如何与遗留应用程序集成。此外,本文还介绍了跨有界上下文进行通信的技巧。
Part II shows you how to integrate bounded contexts, and offers details on the options open for architecting bounded contexts. Code examples are presented that detail how to integrate with legacy applications. Also included are techniques for communicating across bounded contexts.
现代软件应用程序是分布式系统,具有可扩展性和可靠性要求。本章将分布式系统理论与 DDD 相结合,以便您兼得两者之长。
Modern software applications are distributed systems that have scalability and reliability requirements. This chapter blends distributed systems theory with DDD so that you can have the best of both worlds.
构建了一个示例应用程序,展示了如何使用消息总线进行异步消息传递,将分布式系统原理与 DDD 协同应用。
A sample application is built showing how to apply distributed systems principles synergistically with DDD using a message bus for asynchronous messaging.
另一个示例应用程序展示了构建异步分布式系统的另一种方法。此方法使用超文本传输协议 (HTTP)、REST 和 Atom 等标准协议,而不是消息总线。
Another sample application is built showing an alternative approach to building asynchronous distributed systems. This approach uses standard protocols like Hypertext Transport Protocol (HTTP), REST, and Atom instead of a message bus.
第三部分介绍了可用于在代码中构建域模型的设计模式,以及用于持久化模型的模式和用于管理构成模型的域对象的生命周期的模式。
Part III covers the design patterns you can use to build a domain model in code, along with patterns to persist your model and patterns to manage the lifecycles of the domain objects that form your model.
本章将介绍所有可用于构建有效领域模型的战术模式。本章重点介绍一些最佳实践指南,帮助您在代码中生成更易于管理、更具表现力的模型。
This chapter is an introduction to all the tactical patterns at your disposal that allow you to build an effective domain model. The chapter highlights some best practice guidelines that produce more manageable and expressive models in code.
这是对代表金钱等无身份领域概念的 DDD 建模结构的介绍。
This is an introduction to the DDD modeling construct that represents identityless domain concepts like money.
实体是具有身份的领域概念,例如客户、交易和酒店。本章涵盖各种示例和互补的实现模式。
Entities are domain concepts that have an identity, such as customers, transactions, and hotels. This chapter covers a variety of examples and complementary implementation patterns.
一些领域概念是无状态操作,不属于值对象或实体。它们被称为领域服务。
Some domain concepts are stateless operations that do not belong to a value object or an entity. They are known as domain services.
在许多领域中,关注事件比仅仅关注实体更能揭示更深层次的见解。本章介绍领域事件设计模式,它使你可以在领域模型中更清晰地表达事件。
In many domains, focusing on events reveals greater insight than focusing on just entities. This chapter introduces the domain event design pattern that allows you to express events more clearly in your domain model.
聚合是表示领域概念的领域对象的集群。聚合是围绕不变量定义的一致性边界。它们是最强大的战术模式。
Aggregates are clusters of domain objects that represent domain concepts. Aggregates are a consistency boundary defined around invariants. They are the most powerful of the tactical patterns.
工厂是一种生命周期模式,它将复杂域对象的使用与构造分开。
Factories are a lifecycle pattern that separate use from construction for complex domain objects.
存储库在领域模型和底层数据模型之间起到中介作用。它们确保领域模型与任何基础设施问题保持分离。
Repositories mediate between the domain model and the underlying data model. They ensure that the domain model is kept separate from any infrastructure concerns.
与第十八章中的领域事件类似,事件溯源是一种实用的技术,用于在代码中强调问题领域中发生的事件。事件溯源超越了领域事件,它将领域模型的状态存储为事件。本章提供了许多示例,包括使用专用事件存储的示例。
Like domain events in Chapter 18, event sourcing is a useful technique for emphasizing, in code, events that occur in the problem domain. Event sourcing goes beyond domain events by storing the state of the domain model as events. This chapter provides a number of examples, including ones that use a purpose-built event store.
第四部分展示了利用和保护域模型完整性的应用程序架构设计模式。
Part IV showcases the design patterns for architecting applications that utilize and protect the integrity of your domain model.
对于由许多有界上下文组成的系统,用户界面通常需要从多个有界上下文中组合数据,尤其是当您的有界上下文形成分布式系统时。
For systems composed of many bounded contexts, the user interface often requires the composition of data from a number of them, especially when your bounded contexts form a distributed system.
CQRS 是一种设计模式,它创建了两个原本只有一个模型的模型。它不再使用单个模型来处理读写两种不同的上下文,而是创建了两个显式的模型来处理命令或提供报表查询。
CQRS is a design pattern that creates two models where there once was one. Instead of a single model to handle the two different contexts of reads and writes, two explicit models are created to handle commands or serve queries for reports.
了解应用程序和域逻辑之间的区别,以保持模型的集中性和系统的可维护性。
Learn the difference between application and domain logic to keep your model focused and your system maintainable.
业务人员需要信息来做出明智的业务和产品开发决策。本章将演示一系列用于构建报表以增强业务能力的技巧。
Business people need information to make informed business and product-development decisions. A range of techniques for building reports that empower the business is demonstrated in this chapter.
本书介绍了领域驱动设计 (DDD) 背后的主要主题——其实践、模式和原则,以及个人经验和对这一理念的解读。本书旨在为对该理念感兴趣或刚开始接触该理念的人士提供学习辅助。本书并非Eric Evans 所著《领域驱动设计:软件核心复杂性应对之道》(Addison-Wesley Professional,2003 年出版)的替代之作。相反,本书将 Evans 提出的概念提炼成通俗易懂的文字,并结合实际案例,使任何开发人员都能在深入研究该主题之前快速掌握该理念。
This book introduces the main themes behind DDD—its practices, patterns, and principles along with personal experiences and interpretation of the philosophy. It is intended to be used as a learning aid for those interested in or starting out with the philosophy. It is not a replacement for Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans (Addison-Wesley Professional, 2003). Instead, it takes the concepts introduced by Evans and distills them into simple straightforward prose, with practical examples so that any developer can get up to speed with the philosophy before going on to study the subject in more depth.
本书基于作者的个人经验。即使您是一位经验丰富的领域驱动设计 (DDD) 从业者,您可能并不认同本书的观点,但您仍然应该能从中有所收获。
This book is based on the author’s personal experiences with the subject matter. You may not always agree with it if you are a seasoned DDD practitioner, but you should still get something out of it.
在学习本书示例的过程中,您可以选择手动输入所有代码,也可以使用本书附带的源代码文件。本书使用的所有源代码均可从www.wrox.com下载。本书的代码下载位于www.wrox.com/go/domaindrivendesign 的“下载代码”选项卡中。虽然代码示例以 C# .NET 语言编写,但书中的概念和实践可以应用于任何编程语言。
As you work through the examples in this book, you may choose either to type in all the code manually, or to use the source code files that accompany the book. All the source code used in this book is available for download at www.wrox.com. Specifically for this book, the code download is on the Download Code tab at: www.wrox.com/go/domaindrivendesign. Although code examples are presented in C# .NET. The concepts and practices can be applied to any programming language.
您也可以在www.wrox.com上通过 ISBN 搜索本书(本书的 ISBN 为 978-1-1187-1470-6)来查找代码。此外,所有 Wrox 现有图书的完整代码下载列表可在www.wrox.com/dynamic/books/download.aspx获取。
You can also search for the book at www.wrox.com by ISBN (the ISBN for this book is 978-1-1187-1470-6) to find the code. And a complete list of code downloads for all current Wrox books is available at www.wrox.com/dynamic/books/download.aspx.
我们竭尽全力确保文本和代码中没有错误。然而,人无完人,难免会犯错。如果您在我们的书中发现错误,例如拼写错误或代码错误,我们将非常感激您的反馈。发送勘误表,您可以避免其他读者因时间紧迫而感到沮丧,同时也有助于我们提供更高质量的信息。
We make every effort to ensure that there are no errors in the text or in the code. However, no one is perfect, and mistakes do occur. If you find an error in one of our books, like a spelling mistake or faulty piece of code, we would be very grateful for your feedback. By sending in errata, you may save another reader hours of frustration, and at the same time, you will be helping us provide even higher quality information.
要查找本书的勘误表页面,请访问www.wrox.com/go/domaindrivendesign。
To find the errata page for this book, go to www.wrox.com/go/domaindrivendesign.
然后点击“勘误表”链接。在此页面,您可以查看 Wrox 编辑提交并发布的本书所有勘误表。
And click the Errata link. On this page you can view all errata that has been submitted for this book and posted by Wrox editors.
如果您没有在本书的勘误表页面上发现“您的”错误,请访问www.wrox.com/contact/techsupport.shtml并填写表格,将您发现的错误发送给我们。我们会核实信息,并在适当的情况下在本书的勘误表页面上发送一条消息,并在本书的后续版本中修复该问题。
If you don’t spot “your” error on the Book Errata page, go to www.wrox.com/contact/techsupport.shtml and complete the form there to send us the error you have found. We’ll check the information and, if appropriate, post a message to the book’s errata page and fix the problem in subsequent editions of the book.
如需与作者和同行进行讨论,请加入 P2P 论坛 ( http://p2p.wrox.com )。该论坛是一个基于网络的系统,您可以在这里发布与 Wrox 书籍及相关技术相关的帖子,并与其他读者和技术用户互动。论坛提供订阅功能,当论坛上有新帖子时,您会收到您选择的主题的邮件。Wrox 的作者、编辑、其他行业专家以及您的读者朋友都会参与其中。
For author and peer discussion, join the P2P forums at http://p2p.wrox.com. The forums are a web-based system for you to post messages relating to Wrox books and related technologies and interact with other readers and technology users. The forums offer a subscription feature to e-mail you topics of interest of your choosing when new posts are made to the forums. Wrox authors, editors, other industry experts, and your fellow readers are present on these forums.
在http://p2p.wrox.com上,你会发现许多不同的论坛,它们不仅能帮助你阅读本书,还能帮助你开发自己的应用程序。要加入论坛,只需按照以下步骤操作:
At http://p2p.wrox.com, you will find a number of different forums that will help you, not only as you read this book, but also as you develop your own applications. To join the forums, just follow these steps:
加入后,您可以发布新消息并回复其他用户的消息。您可以随时在网上阅读消息。如果您希望通过电子邮件接收某个论坛的新消息,请点击论坛列表中该论坛名称旁边的“订阅此论坛”图标。
Once you join, you can post new messages and respond to messages other users post. You can read messages at any time on the web. If you would like to have new messages from a particular forum e-mailed to you, click the Subscribe to this Forum icon by the forum name in the forum listing.
想了解更多关于如何使用 Wrox P2P 的信息,请务必阅读 P2P 常见问题解答,其中包含论坛软件工作原理以及许多与 P2P 和 Wrox 书籍相关的常见问题。点击任意 P2P 页面上的“常见问题解答”链接即可阅读常见问题解答。
For more information about how to use the Wrox P2P, be sure to read the P2P FAQs for answers to questions about how the forum software works, as well as many common questions specific to P2P and Wrox books. To read the FAQs, click the FAQ link on any P2P page.
本书旨在以务实实用的方式,为经验丰富的开发人员讲解领域驱动设计 (DDD) 的哲学,帮助他们构建复杂领域应用程序。本书重点介绍分解复杂问题空间的原则和实践,以及构建可维护解决方案空间的实现模式和最佳实践。您将学习如何使用战术模式构建有效的领域模型,以及如何通过应用 DDD 的战略模式来保持模型的完整性。
The aim of this book is to present the philosophy of DDD in a down-to-earth and practical manner for experienced developers building applications for complex domains. A focus is placed on the principles and practices of decomposing a complex problem space as well as the implementation patterns and best practices for shaping a maintainable solution space. You will learn how to build effective domain models by using tactical patterns and how to retain their integrity by applying the strategic patterns of DDD.
读完本书,你将对 DDD 有透彻的理解。你将能够阐述它的价值以及何时使用它。你将明白,尽管 DDD 的战术模式很有用,但真正帮助你构建应用程序以进行维护和扩展的,是其原则、实践和战略模式。通过本书中的信息,你将能够更好地管理大型复杂问题域的复杂软件的构建和维护。
By the end of this book, you will have a thorough understanding of DDD. You will be able to communicate its value and when to use it. You will understand that even though the tactical patterns of DDD are useful, it is the principles, practices, and strategic patterns that will help you architect applications for maintenance and scale. With the information gained within this book, you will be in a better place to manage the construction and maintenance of complex software for large and complex problem domains.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
领域驱动设计 (DDD) 是 Eric Evans 在其开创性著作《领域驱动设计:软件核心复杂性应对之道》(Addison-Wesley Professional,2003)中定义的一种开发哲学。DDD 是一种软件开发方法,它使团队能够有效地管理复杂问题领域的软件构建和维护。
Domain-Driven Design (DDD) is a development philosophy defined by Eric Evans in his seminal work Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley Professional, 2003). DDD is an approach to software development that enables teams to effectively manage the construction and maintenance of software for complex problem domains.
本章将为您全面介绍 DDD 的实践、模式和原则,并阐释它如何改进您的软件开发方法。您将了解分析问题空间的价值以及应该将精力集中于何处。您还将理解协作、沟通和上下文对于设计可维护的软件至关重要的原因。
This chapter will give you a high-level introduction to DDD’s practices, patterns, and principles along with an explanation of how it will improve your approach to software development. You will learn the value of analyzing a problem space and where to focus your efforts. You will understand why collaboration, communication, and context are so important for the design of maintainable software.
读完本章,你将对 DDD 有扎实的理解,这将为本书中涵盖的各种模式、实践和原则的细节提供背景。然而,在深入探讨 DDD 如何处理复杂性之前,重要的是先了解哪些问题会导致软件陷入难以管理的状态。
At the end of this chapter you will have a solid understanding of DDD that will provide context to the detail of the various patterns, practices, and principles that are contained throughout this book. However, before we delve into how DDD handles complexity it’s important to understand what problems can cause software to get into an unmanageable state.
要理解 DDD 如何助力复杂领域的软件设计,首先必须了解软件开发和维护的难点。迄今为止,商业应用中最流行的软件架构设计模式是“大泥球”(BBoM)模式。Brian Foote 和 Joseph Yoder 在论文《大泥球》中对 BBoM 的定义是:“……一个结构杂乱、杂乱无章、杂乱无章、如同管道胶带和电线般堆积起来的意大利面条式代码丛林。”
To understand how DDD can help with the design of software for a nontrivial domain, you must first understand the difficulties of creating and maintaining software. By far, the most popular software architectural design pattern for business applications is the Big Ball of Mud (BBoM) pattern. The definition of BBoM, as defined by Brian Foote and Joseph Yoder in the paper “Big Ball of Mud,” is “… a haphazardly structured, sprawling, sloppy, duct-tape-and-baling-wire, spaghetti-code jungle.”
Foote 和 Yoder 使用术语 BBoM 来描述似乎没有可区分架构的应用程序(想象一下一大碗意大利面条和一盘千层面的对比)。当工作流程的常规变化和小的功能增强由于阅读和理解现有代码库的困难而变得难以实现时,允许软件融入 BBoM 的问题就变得显而易见。Eric Evans 在其著作《领域驱动设计:解决软件核心复杂性》(Addison-Wesley Professional,2003 年)中将此类系统描述为包含“做一些有用事情的代码,但没有解释如何做”。软件变得复杂且难以管理的主要原因之一是领域复杂性与技术复杂性的混合,如图1.1所示。
Foote and Yoder use the term BBoM to describe an application that appears to have no distinguishable architecture (think big bowl of spaghetti versus dish of layered lasagna). The issue with allowing software to dissolve into a BBoM becomes apparent when routine changes in workflow and small feature enhancements become a challenge to implement due to the difficulties in reading and understanding the existing codebase. In his book, Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley Professional, 2003), Eric Evans describes such systems as containing “code that does something useful, but without explaining how.” One of the main reasons software becomes complex and difficult to manage is due to the mixing of domain complexities with technical complexities, as illustrated in Figure 1.1.
图 1.1软件的复杂性。
FIGURE 1.1 Complexity in software.
缺乏对共享语言和问题领域知识的关注,会导致代码库虽然能用,但无法展现业务意图。由于分析模型和代码模型之间的转换成本高昂且容易出错,代码库难以阅读和维护。
A lack of focus on a shared language and knowledge of the problem domain results in a codebase that works but does not reveal the intent of the business. This makes codebases difficult to read and maintain because translations between the analysis model and the code model can be costly and error prone.
未绑定到业务可理解的分析模型的代码会随着时间的推移而性能下降,因此更有可能形成类似于 BBoM 模式的架构。由于翻译成本高昂,如果团队在代码中未充分利用问题领域丰富的词汇,那么在与业务专家协作时,发现新领域概念的机会就会降低。
Code without a binding to an analysis model that the business understands will degrade over time and is therefore more likely to result in an architecture that resembles the BBoM pattern. Due to the cost of translation teams that do not utilize the rich vocabulary of the problem domain in code will decrease their chances of discovering new domain concepts when collaborating with business experts.
如图 1.2所示,类似 BBoM 的系统的初始版本开发速度很快,而且通常能取得全面的成功,但由于缺乏围绕问题域模型进行应用程序设计的重点,后续的增强工作也因此变得棘手。代码库缺乏与业务行为所需的协同作用,难以管理变更。问题域的复杂性通常与技术解决方案的偶然复杂性交织在一起。
As highlighted in Figure 1.2, the initial incarnation of a system that resembles BBoM is fast to produce and often a well-rounded success, but because there is little focus based on the design of an application around a model of the problem domain, subsequent enhancements are troublesome. The codebase lacks the required synergy with the business behavior to make change manageable. Complexities of the problem domain are often mixed with the accidental complexities of the technical solution.
图 1.2代码腐烂。
FIGURE 1.2 Code rot.
继续沿用意大利面条式的架构模式会导致功能增强速度缓慢。当产品的新版本发布时,由于开发人员必须处理难以理解的代码库,这些代码库可能会出现大量错误。随着时间的推移,开发团队会越来越抱怨在这种混乱的环境中工作困难。即使项目增加了资源,开发速度也无法提升到满足业务需求的水平。
Continuing to persist with an architectural spaghetti-like pattern can lead to a sluggish pace of feature enhancement. When newer versions of the product are released, they can be buggy due to the unintelligible mess of the codebase that developers have to deal with. Over time, the development team increasingly complains about the difficulty of working in such a mess. Even if resources are added to the project, velocity cannot be increased to a level that satisfies the business.
最终,恼火之下,令人畏惧的应用程序重写请求被批准了。然而,如果没有充分的谨慎和考量,即使是全新的项目也可能重蹈最初BBoM的覆辙。整个过程可能会让企业感到沮丧,因为他们一开始在功能和交付速度方面获得了丰厚的投资回报 (ROI),但随着时间的推移,即使投入了额外的资源,产品也未能持续改进以满足他们的需求。最终,BBoM对开发者来说是个坏消息,因为它是一个混乱且容易出错的代码库,你讨厌处理它。这对企业来说也是一个坏消息,因为它降低了他们快速交付商业价值的能力。
In the end, exasperated by the situation, the request for the dreaded application rewrite is granted. Without due care and consideration, however, even the greenfield project can fall foul of the same issues that created the original BBoM. This entire experience can be frustrating for the business that saw a great return on investment (ROI) in terms of features and speed of delivery at the beginning but over time, even with additional investment in resources, did not see the sustained evolution of the product to meet their needs. Ultimately the BBoM is bad news for you as a developer because it’s a messy bug-prone code base that you hate dealing with. And it’s bad news for the business because it reduces their capability to rapidly deliver business value
如果你对所从事的业务领域了解不够,软件项目就会失败。输入并非交付产品的瓶颈;编码是开发过程中比较容易的部分。除了非功能性需求之外,创建并维护一个能够满足业务用例的、有用的领域软件模型才是难点。然而,你对业务领域的理解越深入,当你尝试用软件建模来解决其固有的业务问题时,你就越有能力。
Software projects fail when you don’t understand the business domain you are working within well enough. Typing is not the bottleneck for delivering a product; coding is the easy part of development. Outside of non-functional requirements creating and keeping a useful software model of the domain that can fulfill business-use cases is the difficult part. However, the more you invest in understanding your business domain the better equipped you will be when you are trying to model it in software to solve its inherent business problems.
DDD 既要应对理解问题域的挑战,又要创建可维护的解决方案来解决其中的问题。它通过利用一系列战略和战术模式来实现这一点。
DDD deals with both the challenge of understanding a problem domain and creating a maintainable solution that is useful to solve problems within it. It achieves this by utilizing a number of strategic and tactical patterns.
DDD 的战略模式提炼问题域并塑造应用程序的架构。
The strategic patterns of DDD distil the problem domain and shape the architecture of an application.
大型软件产品并非都需要完美设计——事实上,试图做到这一点只会浪费精力。开发团队和领域专家会运用分析模式和知识挖掘,将大型问题域提炼成更易于管理的子域。这种提炼揭示了核心子域——软件编写的初衷。核心域是开发中产品背后的驱动力;也是构建产品的根本原因。领域驱动设计 (DDD) 强调需要将精力和才能集中在核心子域上,因为这些领域最具价值,也是应用程序成功的关键。
Not all of a large software product needs be perfectly designed—in fact trying to do so would be a waste of effort. Development teams and domain experts use analysis patterns and knowledge crunching to distill large problem domains into more manageable subdomains. This distillation reveals the core sub domain—the reason the software is being written. The core domain is the driving force behind the product under development; it is the fundamental reason it is being built. DDD emphasizes the need to focus effort and talent on the core subdomain(s) as this is the area that holds the most value and is key to the success of the application.
这种明确精力集中于何处的状态还可以使团队能够为系统中一些不太重要的部分寻找开源的现成解决方案,这意味着他们有更多时间专注于重要的事情并确保核心领域不会成为 BBoM。
This clarity on where to focus effort can also empower teams to look for open source off-the-shelf solutions for some of the less important parts of a system, which means that they have more time to focus on what is important and ensure that the core domain does not become a BBoM.
发现核心领域有助于团队理解他们开发软件的原因,以及软件的成功对业务的意义。正是对业务意图的理解,使开发团队能够识别系统中最重要的部分,并将时间投入其中。随着业务的发展,软件也必须随之发展;它需要具有适应性。对应用程序关键领域的代码质量进行投资,将有助于它随着业务的变化而变化。如果软件的关键领域与业务领域不协调,那么随着时间的推移,设计很可能会腐烂,变成一团泥浆,最终导致软件难以维护。
Discovering the core domain helps teams understand why they’re producing the software and what it means for the software to be successful to the business. It is the appreciation for the business intent that will enable the development team to identify and invest its time in the most important parts of the system. As the business evolves, so in turn must the software; it needs to be adaptable. Investment in code quality for the key areas of an application will help it change with the business. If key areas of the software are not in synergy with the business domain then, over time, it is likely that the design will rot and turn into a big ball of mud, resulting in hard-to-maintain software.
在解决方案空间中,为每个子域构建一个软件模型,用于处理领域问题并使软件与业务轮廓保持一致。该模型并非真实存在的模型,而是一种抽象,旨在满足业务用例的需求,同时仍保留业务领域的规则和逻辑。开发团队应该像关注应用程序的纯技术方面一样,将同等的精力和努力投入到模型和领域逻辑上。为了避免意外的技术复杂性,该模型应与基础设施代码隔离。
In the solution space a software model is built for each subdomain to handle domain problems and to align the software with the business contours. This model is not a model of real life but more an abstraction built to satisfy the requirements of business use cases while still retaining the rules and logic of the business domain. The development team should focus as much energy and effort on the model and domain logic as it does on the pure technical aspects of the application. To avoid accidental technical complexity the model is kept isolated from infrastructure code.
所有模型并非生来平等;最合适的设计模式是根据每个子域的复杂性需求而定的,而不是将一刀切的设计应用于整个系统。对于并非产品成功核心或不太复杂的子域,模型无需基于丰富的面向对象设计,而可以采用更具过程性或数据驱动的架构。
All models are not created equal; the most appropriate design patterns are used based on the complexity needs of each subdomain rather than applying a blanket design to the whole system. Models for subdomains that are not core to the success of the product or that are not as complex need not be based on rich object-oriented designs, and can instead utilize more procedural or data-driven architectures.
模型是通过领域专家和开发团队的协作构建的。沟通是通过一种不断发展的共享语言(称为通用语言 (UL))来实现的,这种语言能够高效地将软件模型连接到概念分析模型。软件模型的结构和类设计使用与 UL 相同的术语,从而将软件模型绑定到分析模型。在编码过程中发现的见解、概念和术语级别在 UL 中复制,因此也复制到分析模型中。同样,当业务在分析模型级别揭示隐藏的概念时,这种洞察也会反馈到代码模型中;这是领域专家和开发团队能够协作改进模型的关键。
Models are built through the collaboration of domain experts and the development team. Communication is achieved using an ever-evolving shared language known as the ubiquitous language (UL) to efficiently and effectively connect a software model to a conceptual analysis model. The software model is bound to the analysis model by using the same terms of the UL for its structure and class design. Insights, concepts, and terms that are discovered at a coding level are replicated in the UL and therefore the analytical model. Likewise when the business reveals hidden concepts at the analysis model level this insight is fed back into the code model; this is the key that enables the domain experts and development teams to evolve the model in collaboration.
模型位于有界上下文中,该上下文定义了模型的适用性并确保其完整性。较大的模型可以拆分为较小的模型,并在单独的有界上下文中定义,以进一步降低复杂性,尤其是在存在术语歧义或多个团队协作的情况下。
Models sit within a bounded context, which defines the applicability of the model and ensures that its integrity is retained. Larger models can be split into smaller models and defined within separate bounded contexts where ambiguity in terminology exists or where multiple teams are a working in order to further reduce complexity.
有界上下文用于在模型周围形成保护性边界,有助于防止软件演变成 BBoM。这是通过允许整体解决方案的不同模型在明确定义的业务环境中演进,而不会对系统的其他部分产生负面的连锁影响来实现的。模型与基础设施代码隔离,以避免合并技术和业务概念时意外产生的复杂性。有界上下文还可以通过将模型与第三方代码隔离来防止模型的完整性受到破坏。
Bounded contexts are used to form a protective boundary around models that helps to prevent software from evolving into a BBoM. This is achieved by allowing the different models of the overall solution to evolve within well-defined business contexts without having a negative, rippling impact on other parts of the system. Models are isolated from infrastructure code to avoid the accidental complexity of merging technical and business concepts. Bounded contexts also prevent the integrity of models being corrupt by isolating them from third-party code.
比较图1.3和图 1.2。该图展示了如何将 DDD 的战略模式应用于软件,以管理大型问题域并保护其中的离散模型。
Compare the diagram in Figure 1.3 to Figure 1.2. The diagram shows how the strategic patterns of DDD have been applied to the software to manage the large problem domain and protect discrete models within it.
图 1.3应用领域驱动设计的战略模式。
FIGURE 1.3 Applying the strategic patterns of Domain-Driven Design.
DDD 深知,为了解决跨子域的领域问题,需要确保团队和业务部门清楚不同的模型和上下文如何协同工作。上下文映射可以帮助您了解全局;它们使团队能够了解存在哪些模型、它们负责什么以及它们的适用范围在哪里。这些映射揭示了不同模型如何交互以及它们如何交换数据以完成业务流程。这些连接之间的关系,更重要的是,它们之间的流程灰色区域,通常不会被业务部门捕捉或理解。
DDD understands the need to ensure that teams and the business are clear on how separate models and contexts work together in order to solve domain problems that span across subdomains. Context maps help you to understand the bigger picture; they enable teams to understand what models exist, what they are responsible for, and where their applicability boundaries are. These maps reveal how different models interact and what data they exchange to fulfill business processes. The relationships between the connections and more importantly the grey area of process that sits between them is often not captured or well understood by the business.
DDD 的战术模式(也称为模型构建块)是一系列模式的集合,有助于为复杂的有界上下文创建有效的模型。战术模式集合中介绍的许多编码模式在 Evans 的著作问世之前就已被广泛采用,并由 Martin Fowler 在《企业应用程序架构模式》和 Erich Gamma 等人在《设计模式:可复用面向对象软件的元素》中进行了分类。这些模式并非适用于所有模型,必须根据其自身特点,结合正确的架构风格来选择。
The tactical patterns of DDD, also known as model building blocks, are a collection of patterns that help to create effective models for complex bounded contexts. Many of the coding patterns presented within the collection of tactical patterns have been widely adopted before Evans’s text and catalogued by the likes of Martin Fowler in Patterns of Enterprise Application Architecture and Erich Gamma, et al. in Design Patterns: Elements of Reusable Object-Oriented Software. These patterns are not applicable to all models, and each must be taken on its own merit with the correct architectural style applied.
本节详述的所有模式都有助于管理问题的复杂性(即问题空间),或者管理解决方案的复杂性(即解决方案空间)。如图 1.4所示,问题空间将问题域提炼为更易于管理的子域。DDD 在问题空间中的作用在于揭示哪些部分是重要的,以及应该将精力集中在哪里。下一章我们将更详细地探讨有助于降低问题空间复杂性的模式。
All of the patterns detailed in this section help to manage the complexity of a problem—aka the problem space or they manage complexity in the solution—aka the solution space. The problem space, as shown in Figure 1.4, distils the problem domain into more manageable subdomains. DDD’s impact in the problem space is to reveal what is important and where to focus effort. In the next chapter we will look in more detail on the patterns that can help reduce complexity in the problem space.
图 1.4适用于问题空间的 DDD 模式。
FIGURE 1.4 DDD patterns that are applicable to the problem space.
DDD 的解决方案方面(如图 1.5所示)涵盖了可以塑造应用程序架构并使其更易于管理的模式。
The solution side of DDD, shown in Figure 1.5, covers patterns that can shape the architecture of your applications and make it easier to manage.
图 1.5适用于解决方案空间的 DDD 模式。
FIGURE 1.5 DDD patterns that are applicable to the solution space.
虽然 DDD 模式众多,但其成功的关键在于一些实践和指导原则。这些构成 DDD 精髓的关键原则常常被忽视,因为人们过于关注用于创建软件模型的战术设计模式。
Whilst there are many patterns of DDD, there are a number of practices and guiding principles that are key to success with its philosophy. These key principles, which form the essence of DDD, are often missed as too much focus is placed upon the tactical design patterns that are used to create software models.
DDD 强调需要将大部分精力集中在核心子域上。核心子域是产品成功与否的关键。它是产品的独特卖点,也是产品被开发而非被购买的原因。核心域是产品中能够赋予您竞争优势并为您的业务创造真正价值的领域。让所有团队成员都理解核心域至关重要。
DDD stresses the need to focus the most effort on the core subdomain. The core subdomain is the area of your product that will be the difference between it being a success and it being a failure. It’s the product’s unique selling point, the reason it is being built rather than bought. The core domain is the area of the product that will give you a competitive advantage and generate real value for your business. It is vital that all of the team understand what the core domain is.
DDD 强调开发团队与业务专家之间协作的重要性,以便构建有效的模型来解决问题。如果没有业务专家的协作和投入,许多知识共享将无法进行,开发团队也无法对问题领域获得更深入的洞察。同样,通过协作和知识整合,业务部门有机会更深入地了解其领域。
DDD stresses the importance of collaboration between the development teams and business experts to produce useful models to solve problems. Without this collaboration and commitment from the business experts, much of the knowledge sharing will not be able to take place, and development teams will not gain deeper insights into the problem domain. It is also true that, through collaboration and knowledge crunching, the business has the opportunity to learn much more about its domain.
DDD 将分析模型和代码模型视为一体。这意味着技术代码模型通过共享 UL 绑定到分析模型。分析模型的突破会导致代码模型的变更。代码模型的重构能够揭示更深入的洞察,这又会反映在分析模型和业务的思维模型中。只有当团队有时间探索模型并对其设计进行实验时,才能取得突破。花时间进行原型设计和实验可以极大地帮助您塑造更好的设计。它还可以揭示糟糕的设计是什么样的。Eric Evans 认为,每一个好的设计都至少有三个糟糕的设计,这将防止团队停留在第一个有用的模型上。
DDD treats the analysis and code models as one. This means that the technical code model is bound to the analysis model through the shared UL. A breakthrough in the analysis model results in a change to the code model. A refactoring in the code model that reveals deeper insight is again reflected in the analysis model and mental models of the business. Breakthroughs only occur when teams are given time to explore a model and experiment with its design. Spending time prototyping and experimenting can go a long way in helping you shape a better design. It can also reveal what a poor design looks like. Eric Evans suggests that for every good design there must be at least three bad ones, this will prevent teams stopping at the first useful model.
能够有效地描述用于表示问题域的模型是 DDD 的基础。正因如此,DDD 最重要的一个方面无疑是创建 UL。如果没有共同的语言,业务团队和开发团队之间解决问题的协作将无法有效进行。团队之间在知识研讨中产生的分析和思维模型需要一种共同的语言来将它们与技术实现绑定。如果没有一种有效的方法来在问题域内交流想法和解决方案,设计突破就无法实现。
The ability to effectively describe a model built to represent a problem domain is the foundation of DDD. This is why, without a doubt, the single most important facet of DDD is the creation of the UL. Without a shared language, collaboration between the business and development teams to solve problems would not be effective. Analysis and mental models produced in knowledge-crunching sessions between the teams need a shared language to bind them to a technical implementation. Without an effective way to communicate ideas and solutions within a problem domain, design breakthroughs cannot occur.
正是 UL 的协作和构建,才使得 DDD 如此强大。它能够更好地理解问题域(对于业务和开发团队而言),并且有效沟通。这些关键价值观对项目有着巨大的影响,因为虽然技术框架和方法论很重要,但 DDD 更重视问题域的分析和理解,而这才是最终决定软件产品成功的关键。
It is the collaboration and construction of a UL that makes DDD so powerful. It enables a greater understanding of the problem domain (for the business and the development team) and more effective communication. These key values have a massive impact on projects because while technical frameworks and methodologies are important, DDD places as much, if not, greater importance on the analysis and understanding of the problem domain that ultimately makes software products successful.
每个构建的模型在其子领域的上下文中都能被理解,并使用基本概念(UL)进行描述。然而,在许多大型模型中,UL 内部可能存在歧义,组织的不同部门对一个通用术语或概念的理解可能有所不同。DDD 通过确保每个模型都有其自己的、仅在特定上下文中有效的 UL 来解决这个问题。每个上下文定义了一个语言边界;确保模型在特定上下文中可被理解,以避免语言上的歧义。因此,一个具有重叠术语的模型会被划分为两个模型,每个模型在其自身上下文中都有明确的定义。在实现方面,战略模式可以强制执行这些语言边界,使模型能够独立发展。这些战略模式可以生成组织有序的代码,支持更改和重写。
Each model that is built is understood within the context of its subdomain and described using the UL. However, in many large models, there can be ambiguity within the UL, with different parts of an organization having different understandings of a common term or concept. DDD addresses this by ensuring that each model has its own UL that is valid only in a certain context. Each context defines a linguistic boundary; ensuring models are understood in a specific context to avoid ambiguity in language. Therefore a model with overlapping terms is divided into two models, each clearly defined within its own context. On the implementation side, strategic patterns can enforce these linguistic boundaries to enable models to evolve in isolation. These strategic patterns result in organized code that is able to support change and rewriting.
任何从事复杂系统的开发人员都能编写优秀的代码并维护一段时间。然而,如果源代码和问题领域之间缺乏协同,持续的开发很可能会最终导致代码库难以修改,从而导致 BBoM(复杂领域模型)。DDD 有助于解决这个问题,它强调团队需要持续关注模型对当前问题的实用性。它要求团队在获得领域洞察后,不断改进和简化复杂的领域模型。DDD 仍然不是灵丹妙药,需要专注和持续的知识积累,才能开发出可维护数年而非数月的软件。新的业务案例可能会破坏先前有用的模型,或者可能需要进行更改以使新的或现有的概念更加明确。
Any developer working on a complex system can write good code and maintain it for a short while. However, without synergy between the source code and the problem domain, continued development will likely end up in a codebase that is hard to modify, resulting in a BBoM. DDD helps with this issue by placing emphasis on the team to continually look at how useful the model is for the current problem. It challenges the team to evolve and simplify complex models of domains as and when it gains domain insights. DDD is still no silver bullet and requires dedication and constant knowledge crunching to produce software that is maintainable for years and not just months. New business cases may break a previously useful model, or may necessitate changes to make new or existing concepts more explicit.
您可以将 DDD 视为一种开发哲学;它倡导一种以领域为中心的新思维方式。DDD 的最大优势在于学习过程,而非最终目标。任何团队都可以编写软件产品来满足一系列用例的需求,但那些在所研究的问题领域投入时间和精力的团队可以持续改进产品,以满足新的业务用例。DDD 本身并非一种严格的方法论,但必须与某种形式的迭代式软件项目方法论结合使用,才能构建和发展有用的模型。
You can think of DDD as a development philosophy; it promotes a new domain-centric way of thinking. It is the learning process, not the end goal, which is the greatest strength of DDD. Any team can write a software product to meet the needs of a set of use cases, but teams that put time and effort into the problem domain they are working on can consistently evolve the product to meet new business use cases. DDD is not a strict methodology in itself but must be used with some form of iterative software project methodology to build and evolve a useful model.
DDD 并非一本关于面向对象设计的书,也不是以代码为中心的哲学或模式语言。然而,如果你在网上搜索 DDD 的文章,你可能会误以为它只是一些实现模式,因为大多数关于 DDD 的文章和博客都侧重于建模模式。对于开发人员来说,更容易看到 DDD 战术模式在代码中的实现,而不是业务用户和团队之间的对话。他们不关心或不理解的领域。这就是为什么 DDD 有时会被误认为仅仅是由实体、值对象和存储库组成的模式语言。事实上,你无需创建丰富的领域模型或使用存储库即可实现 DDD。DDD 与其说是软件设计模式,不如说是通过协作解决问题。
DDD is not a book on object-oriented design, nor is it a code-centric philosophy or a patterns language. However, if you search the web for articles on DDD, you would be mistaken for thinking that it is just a handful of implementation patterns as most articles and blogs on DDD focus on the modeling patterns. It is much easier for developers to see tactical patterns of DDD implemented in code rather than conversations between business users and teams on a domain that they do not care about or do not understand. This is why DDD is sometimes mistakenly thought of as nothing more than a pattern language made up of entities, value objects, and repositories. You can, in fact, implement DDD without ever creating a rich domain model or using a repository. DDD is less about software design patterns and more about problem solving through collaboration.
Evans 介绍了一些使用软件设计模式的技术,使开发团队和业务专家创建的模型能够通过 UL 实现。然而,如果没有分析和协作的实践,编码实现本身就毫无意义。DDD 并非以代码为中心;它的目的并非编写优雅的代码。软件仅仅是 DDD 的产物。
Evans presents techniques to use software design patterns to enable models created by the development team and business experts to be implemented using the UL. However, without the practices of analysis, and collaboration, the coding implementation really means very little on its own. DDD is not code centric; its purpose is not to make elegant code. Software is merely an artifact of DDD.
DDD 不需要特殊的框架或数据库。代码中实现的模型遵循 POCO(Plain Old C# Object,简单老式 C# 对象)原则,确保其不包含任何基础结构代码,从而不会影响其以领域为中心的目的。面向对象方法对于构建模型很有用,但绝非强制性要求。
DDD does not require a special framework or database. The model implemented in code follows a POCO (Plain Old C# Object) principle that ensures it is devoid of any infrastructural code so that nothing distracts from its domain-centric purpose. An object-oriented methodology is useful for constructing models, but it is by no means mandatory.
DDD 与架构无关,这意味着实现 DDD 时无需遵循单一的架构风格。Evans 的著作中提出了一种分层架构风格,但这并非唯一选择。架构风格可以多种多样,因为它们应该应用于有界上下文级别,而不是应用程序级别。一个产品可以包含一个遵循以事件为中心的架构的有界上下文,另一个采用分层富领域模型的有界上下文,以及第三个应用活动记录模式的有界上下文。
DDD is architecturally agnostic in that there is no single architectural style you must follow to implement it. A layered architectural style was presented in Evans’s text, but this is not the only option. Architectural styles can vary because they should apply at the bounded context level and not the application level. A single product can include one bounded context that follows an event-centric architecture, another that utilizes a layered rich domain model, and a third that applies the active record pattern.
DDD 可能需要投入大量精力,它需要迭代式的开发方法、积极的业务投入以及优秀的开发人员。所有软件项目都可以受益于 DDD 的分析实践(例如提炼问题域)以及战略模式(例如隔离代表领域逻辑的代码模型)。然而,并非所有项目都需要 DDD 的战术模式来构建丰富的领域模型。一些简单的领域模型由于缺乏或完全没有领域逻辑,因此无法达到如此复杂的程度。例如,在创建一个简单的博客应用程序时,应用所有 DDD 模式将浪费时间和成本。
DDD can take a lot of effort, it requires an iterative development methodology, an engaged business, and smart developers. All software projects can benefit from the analysis practices of DDD such as distilling the problem domain as well as the strategic patterns such as isolating a code model that represents domain logic. However, not all require the tactical patterns of DDD to build a rich domain model. Trivial domains don’t warrant the level of sophistication as they have little or no domain logic. For example, it would be a waste of time and costly to apply all of the patterns of DDD when creating a simple blogging application.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
要理解复杂的问题领域,从而创建一个简单实用的模型,需要深入的知识和深刻的洞察力,而这只有通过与对该领域了如指掌的人员合作才能获得。在模型设计过程中不断进行实验和探索,才能充分发挥 DDD 的威力。只有通过协作并对问题领域达成共识,才能有效地设计出一个能够应对业务挑战的模型,并且该模型必须足够灵活,能够适应新的需求。
Making sense of a complex problem domain in order to create a simple and useful model requires in-depth knowledge and deep insight that can only be gained through collaboration with the people that understand the domain inside and out. Continuous experimentation and exploration in the design of a model is where the power of DDD is realized. Only through collaboration and a shared understanding of the problem domain can you effectively design a model to solve the challenges of the business that will be supple enough to adapt as new requirements surface.
本章介绍了一些有助于提炼领域知识的方法,以便更好地理解问题领域,从而帮助您构建有效的领域模型。此外,本章还介绍了提取应用程序行为重要信息的方法,以及在问题领域内发现深度洞察的技术。
This chapter introduces methods to facilitate the distilling of domain knowledge in order to better understand the problem domain, which will enable you to build an effective domain model. Methods to extract important information on the behaviors of an application along with techniques to discover deep insights within the problem domain are also presented.
复杂的问题域包含大量信息,其中一些信息并不适用于解决当前问题,只会分散建模工作的注意力。知识挖掘是从问题域中提取相关信息,以构建满足业务用例需求的有用模型的艺术。
Complex problem domains will contain a wealth of information, some of which will not be applicable to solving the problem at hand and will only act to distract from the real focus of your modelling efforts. Knowledge crunching is the art of distilling relevant information from the problem domain in order to build a useful model that can fulfill the needs of business use cases.
在根据一系列需求设计问题域解决方案时,知识梳理是弥补技术团队知识缺口的关键。为了构建有用的模型,团队需要对问题域有深入的洞察,以确保重要的概念不会被忽视或误解。这只能通过与最了解该领域的人员(例如业务用户、利益相关者和领域专家)合作来实现。否则,就有可能产生一个缺乏真正领域洞察力的技术解决方案,并且在软件维护或后续增强过程中,业务部门或其他开发人员无法理解其中的内容。
Knowledge crunching is key to bridging any knowledge gaps for the technical team when designing a solution for a problem domain based on a set of requirements. In order for a team to produce a useful model they need to have a deep insight of the problem domain to ensure important concepts are not overlooked or misunderstood. This can only be done through working in collaboration with the people that understand the domain the most; i.e., the business users, stakeholders, and subject matter experts. Without this there is a danger that a technical solution will be produced that is void of any real domain insight and something that cannot be understood by the business or by other developers during software maintenance or subsequent enhancements.
知识收集过程在白板上进行,与业务专家一起研究示例,并通常进行头脑风暴。它旨在发现并达成对问题领域的共同理解,从而构建一个能够满足业务用例的模型。如图 2.1所示,知识挖掘过程始于系统的行为。团队与业务利益相关者和专家一起讨论应用程序的场景。这一过程是所有参与者进行对话、深入洞察和对领域达成共同理解的催化剂。因此,利益相关者和主题专家的积极参与至关重要。
Knowledge gathering occurs on whiteboards, working through examples with business experts and generally brainstorming together. It is the quest to discover and agree on a shared understanding of the problem domain to produce a model that can fulfill business use cases. The process of knowledge crunching, as shown in Figure 2.1, starts with the behaviors of a system. The team go through the scenarios of the application with the business stakeholders and experts. This process is the catalyst to conversation, deep insight, and a shared understanding of the domain for all participants. It is therefore vital that stakeholders and subject matter experts are actively involved and engaged.
图 2.1知识消化。
FIGURE 2.1 Knowledge crunching.
知识梳理的成果和共享理解的产物是一种通用的通用语言 (UL)。在与利益相关者和领域专家进行建模时,每个人都应有意识地始终如一地应用一种富含领域特定术语的通用语言。这种语言必须明确,并在描述领域模型和问题领域时使用。该语言也应应用于模型的代码实现,并使用与类名、属性和方法名相同的术语和概念。这种语言使业务团队和开发团队能够就软件进行有意义的沟通。您将在第 4 章“模型驱动设计”中了解有关 UL 的更多信息。
An output of knowledge crunching and an artifact of the shared understanding is a common Ubiquitous Language (UL). When modeling with stakeholders and subject matter experts everyone should make a conscious effort to consistently apply a shared language rich in domain-specific terminology. This language must be made explicit and be used when describing the domain model and problem domain. The language should also be used in the code implementation of the model, with the same terms and concepts used as class names, properties, and method names. It is the language that enables both the business and development teams to have meaningful communication about the software. You will learn more about the UL in Chapter 4, “Model-Driven Design.”
UL 用于将模型的代码表示与以业务人员能够理解的语言和图表形式传达的概念模型绑定在一起。UL 将包含业务术语以及在对问题域用例进行建模时发现的新概念和术语。UL 的共同理解避免了不断地从技术模型转换为业务模型,从而消除了重要洞察的丢失风险。
UL is used to bind the code representation of the model to the conceptual model communicated in language and diagrams, which the business can understand. The UL will contain terminology from the business as well as new concepts and terms discovered when modeling the use case of the problem domain. The shared understanding that a UL prevents the need to constantly translate from a technical model to a business model, thus removing the chance of vital insight being lost.
领域知识至关重要,甚至比技术诀窍更为关键。在流程和逻辑复杂的业务中工作的团队需要深入问题领域,像海绵一样吸收所有相关的领域知识。这种洞察力将使团队能够专注于关键点,并在应用程序代码库的核心创建一个能够满足业务用例并在应用程序的整个生命周期内持续发挥作用的模型。
Domain knowledge is key, even more so than technical know-how. Teams working in a business with complex processes and logic need to immerse themselves in the problem domain and, like a sponge, absorb all the relevant domain knowledge. This insight will enable teams to focus on the salient points and create a model at the heart of their application’s code base that can fulfill the business use cases and keep doing so over the lifetime of the application.
如果您无法用简单的语言与业务用户解释问题域中的复杂概念,那么您还没有准备好开始在该领域开发软件。为了能够持续快速地更新您正在构建的复杂应用程序,即使业务不断变化,功能也需要不断更新,您需要在设计和开发过程中重新调整工作重点——您需要让您的团队专注于业务问题,而不仅仅是技术。
If you can’t speak to your business users in simple terms about complex concepts in the problem domain, you are not ready to start developing software within it. To constantly deliver updates at a rapid pace on the complex applications you are building, even as the whimsical business keeps chopping and changing features, you need to refocus your efforts during design and development—you need to focus your team on the business’s problems and not just technologies.
在 DDD 的世界里,传统业务分析师的角色似乎已不复存在;然而,事实并非如此。业务分析师仍然可以帮助利益相关者充实他们的初步想法,并捕捉产品的输入和输出。如果您手下有一些奇葩的开发人员,并且不敢让他们与领域专家面对面交流,您也可以聘请业务分析师作为沟通的引导者。您肯定不希望切断开发团队与最了解该业务领域的人员之间的直接沟通。
It may seem that the role of the traditional business analyst is no longer required in the world of DDD; however, this is not the case. A business analyst can still help stakeholders flesh out their initial ideas and capture inputs and outputs of the product. If you have odd whiz-kid developers and are nervous about putting them in front of domain experts, you can also use business analysts as facilitators to help communication. What you don’t want to do is remove the direct communication between the development team and the people who understand that part of the business the most.
知识挖掘是一个持续的过程;团队应该持续努力,以简化问题领域的视角,只关注相关部分,以帮助创建有用的模型。正如您将在第4章中了解到的,模型驱动设计和领域模型的演进是一个持续的过程。为了确保您拥有一个适用于系统当前用例的有用模型,必须舍弃许多模型。开发团队、业务利益相关者和主题专家之间的协作不应局限于项目初期。知识挖掘应该成为业务部门在应用程序构建的整个生命周期中持续关注的问题。
Knowledge crunching is an ongoing process; teams should continually be working toward a simple view of the problem domain that focuses only on the relevant pieces to aid the creation of a useful model. As you will learn in Chapter 4, model-driven design and the evolution of a domain model is an ongoing process. Many models must be rejected in order to ensure you have a useful model for the current use cases of a system. Collaboration between the development team, business stakeholders, and subject matter experts should not be constrained to the start of a project. Knowledge crunching should be an ongoing concern with the business engaged throughout the lifetime of the application build.
重要的是要意识到,随着系统的每次迭代,模型都会不断发展。它会随着新需求的增加而变化。新的行为和用例将需要对模型进行修改,因此技术团队和业务专家必须明白,模型永远不会完成;它只对当前的问题有用。
It is important to realize also that with each iteration of the system the model will evolve. It will change as and when new requirements are added. New behaviors and use cases will require changes to the model therefore it is important for the technical team and the business experts to understand that the model is never done; it is only useful for the problems at hand.
随着每次迭代的推进,团队对问题领域的理解不断加深。这将带来更深入的洞察和设计突破,从而极大地简化模型。在系统投入使用时,模型可能还需要由于技术原因(例如性能或更好地理解系统使用情况)而进行改进。一个好的模型应该能够灵活地应对变化;一个成熟的模型应该包含丰富且富有表现力的问题领域概念和术语,并且能够被业务团队和技术团队理解。
As each iteration passes the team’s understanding of the problem domain improves. This leads to deeper insight and design breakthroughs that can vastly simplify a model. When the system is in use the model may also need to evolve due to technical reasons such as performance or better understanding of the systems usage. A good model is one that is supple to change; a mature model holds rich and expressive concepts and terminology of the problem domain and is understood by both the business and technical teams.
业务团队和开发团队之间的协作是 DDD 的一个重要方面,对于正在开发的产品的成功至关重要。但是,找到您所在领域的主题专家并能为您提供有关问题领域的更深入见解至关重要。DDD 将这些主题专家称为领域专家。领域专家是深入了解业务领域(从其策略和工作流程到其麻烦和特质)的人。他们是该领域业务的专家;他们很少(如果有的话)拥有领域专家的头衔。相反,您应该寻找产品所有者、用户以及任何对您所在领域有深入掌握和理解的人,无论其头衔如何。
The collaboration between the business and the development team is an essential aspect of DDD and one that is crucial to the success of a product under development. However, it is important to seek out those who are subject matter experts in the domain you are working in and who can offer you deeper insight into the problem area. DDD refers to these subject matter experts as domain experts. The domain experts are the people who deeply understand the business domain from its policies and work flows, to its nuisances and idiosyncrasies. They are the experts within the business of the domain; they will rarely, if ever, have the title of domain expert. Instead, look for the product owners, users, and anyone who has a great grasp and understanding for the domain you are working in regardless of title.
问题空间为您提供一组需求、输入和预期输出——这些通常由您的利益相关者提供。解决方案空间包含一个能够满足需求的模型——这正是领域专家可以提供帮助的地方。
A problem space gives you a set of requirements, inputs, and expected outputs—this is usually provided from your stakeholders. A solution space contains a model that can meet the needs of the requirements—this is where domain experts can help.
如图 2.2所示,如果您的利益相关者不是领域专家,那么他的角色将与领域专家的角色大不相同。利益相关者会告诉您他们希望系统做什么;他们会关注输入和输出。而领域专家则会与您合作,以生成一个有用的模型,该模型既能满足利益相关者的需求,又能满足应用程序的行为。
As shown in Figure 2.2 if your stakeholder is not a domain expert then his role will differ greatly from that of a domain expert. A stakeholder will tell you what they want the system to do; they will focus on the inputs and outputs. A domain expert on the other hand will work with you in order to produce a useful model that can satisfy the needs of a stakeholder and the behaviors of the application.
图 2.2利益相关者、领域专家和开发团队的角色。
FIGURE 2.2 The roles of stakeholders, domain experts, and the development team.
与领域专家合作,不仅能让开发团队了解他们正在处理的问题领域,还能帮助领域专家完善自己对该领域的理解。业务部门可能隐性理解的概念,现在由开发团队和领域专家明确定义,从而改善业务部门内部的沟通。
Working with a domain expert will not only enable development teams to gain knowledge about the problem domain that they are working in but also help the domain expert to qualify her understanding of the domain. Concepts that may have been implicitly understood by the business are explicitly defined by the development team and domain expert, which leads to improved communication within the business itself.
为了实现高水平的协作,建议为开发团队安排领域专家,让他们能够在走廊或休息室的临时会议上解答问题并参与分析;如果沟通仅限于每周的项目会议,就会失去这些。协作是领域驱动设计 (DDD) 的重要组成部分;没有协作,许多设计突破就无法实现。正是这种更深入的设计洞察,使软件能够有效运作,并适应业务流程的变化。
To enable a high level of collaboration, it is recommended that you collocate the development team with domain experts who will be on hand to answer questions and participate during analysis at impromptu corridor or break room meetings; that’s something that is lost when the communication is restricted to weekly project meetings. Collaboration is such an essential part of DDD; without it, a lot of the design breakthroughs would not happen. It is this deeper design insight that makes the software useful and able to adapt when the business processes change.
如果无法与你的领域专家坐在一起,那就和她一起吃午饭。尽可能多地和她在一起,尽可能多地学习。如果你致力于交付高质量的软件来满足个人需求或实现职业目标,那么就把吃饭、睡觉、呼吸都投入到这个领域——你将获得巨大的回报。
If it’s not possible to sit with your domain expert, join her for lunch. Spend as much time as you can with her, and learn as much as possible. If you thrive on delivering high-quality software for personal satisfaction or career goals, then eat, sleep, and breathe the domain—you will be immensely rewarded
创建实用的模型是一种协作体验;然而,业务用户也可能觉得它很累,而且效率低下。业务用户通常都很忙。为了让您的知识交流环节充满乐趣和互动性,您可以引入一些引导游戏和其他形式的需求收集活动来吸引业务用户的参与。
Creating a useful model is a collaborative experience; however, business users can also find it tiring and can deem it unproductive. Business users are busy people. To make your knowledge-crunching session fun and interactive, you can introduce some facilitation games and other forms of requirement gathering to engage your business users.
不要让领域专家和业务利益相关者逐一过一遍需求清单,让他们感到厌烦。如前所述,领域专家的时间非常宝贵。从那些让业务部门夜不能寐的问题领域开始,这些领域将对……产生重大影响。业务的关键在于哪些因素是应用程序成功的关键。例如,询问领域专家当前系统中哪些部分难以使用,哪些手动流程阻碍了他们开展更具创造性、增值性的工作。或者,哪些变革可以增加收入、提高运营效率并节省成本。追踪资金流向,寻找哪些领域正在浪费业务成本或阻碍收入增长,通常是一个好主意。最有趣的对话将揭示您应该在哪些方面投入更多精力来建立共识和共同语言。
Don’t bore domain experts and business stakeholders by going through a list of requirements with them one item at a time. As stated before, a domain expert’s time is precious. Start with the areas of the problem domain that keep the business up at night—the areas that will make a difference to the business and that are core for the application to be a success. For example, asking the domain experts which parts of the current system are hard to use, or which manual processes stop them from doing more creative, value-adding work. Or what changes would increase revenue or improve operational efficiencies and save money from the bottom line. It’s often a good idea to follow the money and look for the areas that are costing the business money or preventing them from increasing revenue. The most interesting conversations will reveal where you should spend most of your effort on creating a shared understanding and a shared language.
尝试理解新领域时,最好的起点是绘制用例图。用例列出了实现目标所需的步骤,包括用户与系统之间的交互。与业务用户合作,了解用户如何使用当前系统,无论是纸质流程还是计算机化流程。仔细聆听领域术语,因为这构成了描述和沟通问题领域的通用语言的开端。用你自己的理解向领域专家复述用例图也很有用,这样他们就能验证你是否和他们一样理解了用例。记住:绘制现实的流程图,理解工作流程的本来面目,在真正理解和把握问题之前,不要急于跳到解决方案。
The best place to start when trying to understand a new domain is by mapping out use cases. A use case lists the steps required to achieve a goal, including the interactions between users and systems. Work with business users to understand what users do with the current system, be it a paper-based process or one that’s computerized. Be careful to listen for domain terminology, because this forms the start of your shared language for describing and communicating the problem domain. It’s also useful to read back the use case to the domain expert in your own understanding, so they can validate that you do understand the use case as they do. Remember: capture a process map of reality, understand the work flow as it is, and don’t try to jump to a solution too quickly before you truly understand and appreciate the problem.
好的产品是什么样的?这款产品的成功标准是什么?什么让它值得投入?企业想要实现什么目标?在知识问答环节中提出的问题将极大地帮助你理解正在开发的产品的重要性及其背后的意图。
What does good look like? What is the success criteria of this product? What will make it a worthwhile endeavor? What is the business trying to achieve? The questions you ask during knowledge-crunching sessions will go a long way toward your understanding of the importance of the product you are building and the intent behind it.
以下是一些示例,可以让您的领域专家进行讨论并揭示对该领域的更深入的见解:
Here are some examples to get your domain expert talking and revealing some deeper insight into the domain:
人们通常通过观察所讨论概念的视觉表达来更快地学习。绘制简单的图表是领域驱动设计 (DDD) 从业者常用的一种可视化技巧,可以增强知识的消化,并最大限度地利用与领域专家交流的时间。
People often learn quicker by seeing visual representations of the concepts they are discussing. Sketching simple diagrams is a common visualization technique DDD practitioners use to enhance knowledge-crunching sessions and maximize their time with domain experts.
你可以先在白板或纸上画草图。只要保持快速和非正式的表达,就能随着讨论的进展快速迭代。
You can start by drawing sketches on the whiteboard or on paper. If you keep them quick and informal you can quickly iterate on them as the conversation progresses.
遗憾的是,许多开发人员发现创建有效的图表非常困难。然而,在绘制草图时,一个基本原则可以帮助您创建高效的图表:保持图表的细节层次一致。如果您讨论的是高级概念,例如独立软件系统如何通信以实现业务用例,请尽量避免深入到类或模块名称等会使图表变得混乱的低级概念。保持图表的细节层次一致可以避免显示过多或过少的细节,从而确保每个人都能理解您想要表达的内容。通常,最好创建多个不同细节层次的图表。
Unfortunately, many developers find it difficult to create effective diagrams. However, when drawing sketches, one basic principle can help you to create highly effective diagrams: keep your diagrams at a consistent level of detail. If you’re talking about high-level concepts like the way independent software systems communicate to fulfill a business use case, try not to drop down into lower-level concepts like class or module names that will clutter the diagram. Keeping your diagrams at a consistent level of detail will prevent you from showing too much detail or too little detail, meaning everyone can understand what you are trying to convey. It’s often better to create multiple diagrams each at a different level of detail.
UML 是一种非常棒的语言,即使你缺乏技术专业知识,也能用它以通俗易懂的方式交流复杂的系统。然而,对于快速的知识梳理会议来说,它可能过于正式,因为团队需要反复尝试和建模。不要尝试使用 Visio 或 Rational Rose 等复杂的软件来捕捉动态模型。相反,你应该在白板上画出模型草图。相比于用 Visio 画出一个耗时半天的图表,你更不会对花几分钟时间画的草图感到执着。如果你必须记录你的知识梳理会议,请在你对问题领域了解最多的时候再写。
UML is a wonderful language you can use to communicate complex systems in an understandable manner with little or no technical expertise. However it maybe too formal for rapid knowledge-crunching sessions, as the team will need to retry and model many times. Don’t try to use elaborate packages such as Visio or Rational Rose to capture a moving model. Instead, sketch out the model on the whiteboard. You will be less attached to a sketch that took you minutes to draw than a diagram in Visio that took you most of the morning. If you must write up your knowledge-crunching sessions, do it at the end when you know the most about the problem domain.
以可视化的方式捕捉信息是快速传达想法和概念的有效方法。然而,由于 DDD 是围绕共享语言的核心思想构建的,因此使用专注于创建简洁而强大的语言的知识收集技术至关重要。
Capturing information visually is an effective way to quickly communicate ideas and concepts. However, because DDD is built around the core idea of a shared language, it is important to use knowledge-gathering techniques that focus on creating a concise and powerful language.
CRC(类职责协作)卡片分为三个区域,包含以下信息:
CRC (Class Responsibility Collaboration) cards are divided into three areas and contain the following information:
CRC 卡让团队和业务专家集中思考问题领域中概念的语言。
CRC cards focus the team and the business experts on thinking about the language of the concepts in the problem domain.
在对领域进行建模时,命名非常重要。但是,当您通过进一步的知识梳理发现概念与您最初的理解不同时,过早命名可能会带来问题。问题在于与最初选定的名称的关联,以及它如何影响您的思维。Greg Young 建议(http://codebetter.com/gregyoung/2012/02/28/the-gibberish-game-4/)使用胡言乱语来表示您不确定的模型区域。我倾向于使用颜色,但想法是一样的。不要给模型的区域或概念赋予真实名称,而是使用胡言乱语,直到您理解了关注点的所有职责、行为和数据。推迟对模型中的概念进行命名将大大有助于您避免对您正在试图改变的现实进行建模以使业务受益。
Naming is important when modeling a domain. However, premature naming can be a problem when you discover through further knowledge crunching that concepts turn out to be different from what you first understood them to be. The issue is the association with the word that was chosen as the original name and the way it shapes your thinking. Greg Young suggests (http://codebetter.com/gregyoung/2012/02/28/the-gibberish-game-4/) making up words for areas of the model you are not sure about, using gibberish. I tend to favor using colors, but the idea is the same. Instead of giving areas or concepts of the model real names, use gibberish until you have understood all the responsibilities, behavior, and data of a concern. Deferring the naming of concepts in your model will go a long way toward helping you avoid modeling a reality that you are trying to change to the business’s benefit.
还要注意术语重载。要避免使用 XXXXService 和 XXXXManager 等类型的名称。如果您发现自己在类或概念后附加了服务或管理器,请更具创造性地思考,努力理解名称背后的真正含义。当您觉得自己真正理解了模型的某个部分时,就能更好地为其赋予一个合理且有意义的名称。
Also look out for overloaded terms. The types of names that you want to avoid are XXXXService and XXXXManager. If you find yourself appending service or manager to a class or concept, think more creatively, strive for real intent behind a name. When you feel you have really understood a part of the model, you will be in a better place to give it a sensible and meaningful name.
行为驱动开发 (BDD) 是一种基于测试驱动开发 (TDD) 的软件开发过程,它专注于捕捉系统的行为,然后从外部推动设计。BDD 在与领域专家和利益相关者的对话中使用具体的领域场景来描述系统的行为。
Behavior-Driven Development (BDD) is a software development process, based on Test-Driven Development (TDD), which focuses on capturing the behavior of a system and then driving design from the outside in. BDD uses concrete domain scenarios during conversations with domain experts and stakeholders to describe the behaviors of a system.
与 DDD 类似,BDD 并不关注应用程序的技术层面。两者的区别在于,BDD 关注的是软件行为,即系统应该如何运行;而 DDD 则关注的是软件核心的领域模型,用于实现这些行为——这是一个细微但重要的区别。
Much like DDD, BDD does not focus on the technical aspects of an application. Where it differs is that BDD focuses on the software behavior, how the system should behave, whereas DDD focuses on the domain model at the heart of the software that is used to fulfil the behaviors—a small but important distinction.
BDD 有其自己的 UL 形式来指定需求——一种分析语言,或者说,GWT(Given, When, Then)。GWT 格式有助于构建与领域专家的对话,并揭示领域的实际行为。
BDD has its own form of UL to specify requirements—an analysis language, if you will, known as GWT (Given, When, Then). The GWT format helps to structure conversations with domain experts and reveal the real behaviors of a domain.
为了演示如何使用 BDD,请查看如何利用用户故事捕获产品需求。
To demonstrate how BDD is used, look at how the requirements for a product are captured utilizing user stories.
电子商务网站功能示例:
An example of a feature for an e-commerce site:
特色:大额订单免费送货
Feature: Free Delivery for Large Orders
此功能的一些故事可能是:
Some stories for this feature may be:
为了提高平均订单总额(50 美元),
In order to increase the average order total, which is $50,
作为营销经理
As the marketing manager
如果顾客消费 60 美元,我愿意提供免费送货服务。
I would like to offer free delivery if customers spend $60.
此功能的另一个故事示例:
Another story example for this feature:
为了针对具有不同消费习惯的不同国家,
In order to target different countries that have different spending habits,
作为营销经理
As the marketing manager
我想为每个配送国家/地区设置合格订单总数门槛。
I would like to set the qualifying order total threshold for each delivery country.
功能描述的是能够为业务带来价值的行为。功能故事中还会包含角色和优势。清晰地描述与功能相关的角色,能够帮助开发团队以及领域专家了解应该与谁沟通或由谁代理。优势能够证明该功能存在的合理性,有助于阐明业务用户为何需要该功能。
A feature describes a behavior that gives value to the business. In a feature story, a role and benefit are also included. The clarity of the role that relates to the feature enables the development team, along with domain experts, to understand who to talk to or who to proxy. The benefit justifies the existence of the feature, helping to clarify why the business user wants the feature.
为了更好地理解某个功能及其行为,可以使用 BDD 场景来描述该功能在不同用例下的行为。场景始于一个初始条件(Givens)。然后,场景包含一个或多个事件(Whens),最后描述预期结果(Thens)。
To better understand a feature and its behavior, use BDD scenarios to describe the feature under different use cases. Scenarios start with an initial condition, the Givens. Scenarios then contain one or more events, the Whens, and then describe the expected outcomes, the Thens.
BDD 场景示例:
An example of a BDD scenario:
场景:客户满足免费送货的消费门槛
Scenario: Customer satisfies the spend threshold for free delivery
假设:免费送货的门槛设定为 60 美元
Given: Threshold for free delivery is set at $60
并且:我是一名顾客,购物篮总额为 50 美元
And: I am a customer who has a basket totaling $50
时间:我将一件价值 11 美元的商品添加到购物车
When: I add an item to my basket costing $11
然后:我应该享受免费送货服务
Then: I should be offered free delivery
进一步的例子:
A further example:
场景:客户未达到免费送货的消费门槛,但触发了追加销售消息
Scenario: Customer does not satisfy the spend threshold for free delivery but triggers message to up sale
假设:免费送货的门槛设定为 60 美元
Given: Threshold for free delivery is set at $60
并且:我是一名顾客,购物篮总额为 50 美元
And: I am a customer who has a basket totaling $50
时间:我将一件价值 9 美元的商品添加到购物车
When: I add an item to my basket costing $9
然后:我应该被告知,如果我的购物篮商品总额增加 1.00 美元,我将享受免费送货服务
Then: I should be told that if I increase my basket goods total by $1.00, I will be offered free delivery
除了作为捕获需求的轻松方式之外,场景还提供了验收标准,开发人员和测试人员可以使用它来确定功能何时完成,业务用户可以使用它来确认团队是否理解该功能。
In addition to being a light way of capturing requirements, the scenarios provide acceptance criteria that developers and testers can use to determine when a feature is complete, and business users can use to confirm that the team understands the feature.
使用这种捕获需求的方法可以消除传统需求文档可能造成的歧义,同时还能显著强调领域语言。功能和场景是开发团队和业务专家协作的成果,有助于塑造最终用户定义框架 (UL)。
Using this method of capturing requirements removes the ambiguity that traditional requirement documentation can result in while also heavily emphasizing the domain language. The features and scenarios are a product of collaboration between the development team and business experts, and can help to shape the UL.
在知识研讨环节,优先考虑快速原型设计。业务用户最喜欢屏幕模型,因为它们能更深入地揭示产品背后的意图。用户理解用户界面;他们可以与之交互,并清晰地执行工作流程。
Favor rapid prototyping during knowledge-crunching sessions. Business users like nothing more than screen mock-ups, because they reveal so much more about the intent they have behind a product. Users understand UI; they can interact with it and act out work flows clearly.
快速原型设计的另一种形式是用代码捕获需求。Greg Young 将这种代码称为分析;他有一个关于此主题的演示文稿,我们可以在这里访问:http://skillsmatter.com/podcast/open-source-dot-net/mystery-ddd。同样,业务用户会喜欢你在他们眼前编写和创作。开始编写代码有助于集中精力进行分析。开始将知识梳理中的抽象想法付诸实践,将使你能够验证和证明你的模型。这也有助于避免只停留在抽象思维,因为抽象思维可能会导致分析瘫痪(http://sourcemaking.com/antipatterns/analysis-paralysis)。
Another form of rapid prototyping is to capture requirements in code. Greg Young calls this code as analysis; he has a presentation on this topic we can access here: http://skillsmatter.com/podcast/open-source-dot-net/mystery-ddd. Again, business users will love the fact that you are writing and creating before their eyes. Starting to code will help focus analysis sessions. Starting to implement abstract ideas from knowledge crunching will enable you to validate and prove your model. It also helps to avoid only abstract thinking, which can lead to analysis paralysis (http://sourcemaking.com/antipatterns/analysis-paralysis).
快速编码有助于创建有力的问题,并有助于找到缺失的用例。使用代码来识别和解决问题。经过大约一个小时的讨论后,看看能否为你的头脑风暴创建一个代码模型。我经常发现,用代码实现想法有助于巩固领域概念并验证模型设计。这个过程有助于保持开发团队的参与度,并让他们全神贯注于学习领域知识,因为他们可以立即获得关于设计的反馈。
Coding quickly helps create your powerful questions and helps find missing use cases. Use the code to identify and solve the problems. After an hour or so of talking, see if you can create a code model of your brainstorming. I often find that implementing ideas in code helps to cement domain concepts and prove model designs. This process helps to keep the development team engaged and deeply engrossed in learning about the domain as they can start to get feedback on a design immediately.
记住:只创建与特定问题相关的代码模型,并使其符合特定的上下文环境;你无法有效地对整个领域进行建模。从小处着眼,围绕规则进行建模;然后逐步构建。最重要的是,记住你编写的是一次性代码。不要止步于第一个有用的模型,也不要过于执着于你的第一个好主意。
Remember: Only create a code model of what is relevant and within the specific context to solve a given problem; you can’t effectively model the entire domain. Think small and model around rules; then build up. Most important, remember that you are writing throw-away code. Don’t stop at the first useful model, and don’t get too attached to your first good idea.
如果您正在为一个尚未有软件解决方案的问题领域开发解决方案,请关注企业在当前纸质解决方案中如何使用语言。某些流程和工作流程可能无法从处理边缘情况的复杂模型中获益。对于罕见的边缘情况,将权力交还给手动流程或许能更好地解决;建模可能会耗费大量精力,却收效甚微。
If you are developing a solution for a problem domain that does not have an existing software solution, look to how the business uses language in the current paper-based solution. Some processes and work flows may not benefit from an elaborate model to handle edge cases. Rare edge-case scenarios may be better solved by handing power back to the manual process; modeling this may result in a lot of effort for little business value.
有时你无需重新发明轮子。如果你在一个已经存在很久的领域工作,比如金融机构,你肯定它遵循一个已知的模型。你没有时间成为你所在领域的专家,所以要寻找能让你更深入地了解该领域的信息。Martin Fowler的《分析模式:可重用对象模型》(Addison-Wesley,1996)介绍了各种领域的许多常见模型,你可以将其作为讨论的起点。
Sometimes you don’t need to reinvent the wheel. If you are working in a domain that has been around for a long time, such as a financial institution, you can bet that it probably follows a known model. You won’t have time to become an expert in your problem domain, so seek out information that teaches you more about the domain. Analysis Patterns: Reusable Object Models by Martin Fowler (Addison-Wesley, 1996) presents many common models in a variety of domains that you can use as a starting point in discussions.
组织中可能已经存在问题领域的模型。索取现有的流程图和工作流程图,以帮助您更深入地理解该领域。创建一个类似维基百科的知识库,其中包含与团队共享的术语和定义。请记住,您的水平取决于最弱的开发人员;这不仅适用于领域知识,也适用于技术专长。
Models of the problem domain could already exist in the organization. Ask for existing process maps and work flow diagrams to help you understand the domain at a deeper level. Create a knowledge base like a wiki with terms and definitions to share with the team. Remember that you are only as good as your weakest developer; this applies to domain knowledge as much as technical expertise.
警惕客户对现有软件的增强要求,因为他们通常会根据现有系统的局限性而不是他们真正的需求提出要求。问问自己,你有多少次与用户沟通,真正了解了需求背后的动机。你是否理解了需求背后的原因?一旦你分享并理解了客户的真正需求,你通常就能提出更好的解决方案。当你像这样与客户沟通,然后紧接着一句经典台词:“哦,真的吗?我不知道。”“你能做到!”记住:你是赋能者。不要盲目遵循用户的需求。业务用户可能无法编写有效的功能或有效地表达目标。你必须分享并理解底层愿景,并了解业务部门想要实现的目标,这样你才能创造真正的商业价值。
Be wary of customers asking for enhancements to existing software, because they will often give you requirements that are based on the constraints of the current systems rather than what they really desire. Ask yourself how often you have engaged with a user to really find the motivation behind a requirement. Have you understood the why behind the what? Once you share and understand the real needs of a customer, you can often present a better solution. Customers are usually surprised when you engage them like this, quickly followed by the classic line: “Oh, really? I didn’t know you could do that!” Remember: You are the enabler. Don’t blindly follow the user’s requirements. Business users may not be able to write effective features or effectively express goals. You must share and understand the underlying vision and be aware of what the business is trying to achieve so you can create real business value.
事件风暴是一项研讨会活动,旨在以有趣且引人入胜的方式,帮助业务团队和开发团队快速理解问题领域。领域专家(拥有答案的人)和开发团队成员(拥有疑问的人)共同协作,对问题领域达成共识。知识碰撞发生在一个开放的环境中,该环境拥有充足的空间用于可视化建模,无论是大量的白板还是无尽的牛皮纸卷。
Event Storming is a workshop activity that is designed to quickly build an understanding of a problem domain in a fun and engaging way for the business and development teams. Groups of domain experts, the ones with the answers, and development team members, the ones with the questions, work together to build a shared understanding of the problem domain. Knowledge crunching occurs in an open environment that has plenty of space for visual modeling, be that lots of whiteboards or an endless roll of brown paper.
问题域的探索始于领域事件;即发生在问题域中且业务关注的事件。将代表领域事件的便利贴添加到绘图表面,然后关注该事件的触发因素。事件可能由用户操作引发,该操作被捕获并作为命令添加到绘图表面。外部系统或其他事件可能是事件的发起者;这些也会被添加到画布中。此活动持续到没有其他问题为止。然后,团队可以开始围绕事件的决策点以及它们何时产生新事件来构建模型。
The problem domain is explored by starting with a domain event; i.e., events that occur within the problem domain that the business cares about. A Post-it note representing the domain event is added to the drawing surface and then attention is given to the trigger of that event. An event could be caused by a user action that is captured and added to the surface as a command. An external system or another event could be the originator of the event; these are also added to the canvas. This activity continues until there are no more questions. The team can then start to build a model around the decision points that are made about events and when they, in turn, produce new events.
事件风暴对于培养 UL 来说是一项极其有用的活动,因为每个事件和命令都有明确的名称,这有助于在开发人员和业务专家之间达成共识。它还可以揭示问题域的子域和核心域,这将在第 3 章“关注核心域”中详细介绍。然而,它最大的好处在于它有趣、引人入胜,而且可以快速完成。这项活动由 Alberto Brandolini 创建,更多信息请访问他的博客http://ziobrando.blogspot.co.uk/。
Event storming is an extremely useful activity for cultivating a UL as each event and command is explicitly named, this goes a long way to producing a shared understating between the developers and business experts. It can also reveal sub domains and the core domain of the problem domain, which will be covered in detail in Chapter 3, “Focusing on the Core Domain.” The biggest benefit however is that it’s fun, engaging, and can be done quickly. Alberto Brandolini created this activity and more information can be found on his blog at http://ziobrando.blogspot.co.uk/.
一种能够更好地理解业务利益相关者意图的新技术是影响图。通过影响图,您可以超越传统的需求文档,尝试了解企业试图产生的影响。他们想要增加销售额吗?他们的目标是扩大市场份额吗?他们想进入一个新市场吗?也许他们想提高参与度,以创造更多具有更高终身价值的忠诚客户。
A new technique for better understanding the intent of business stakeholders is impact mapping. With impact mapping, you go beyond a traditional requirements document and instead you try to work out what impacts the business is trying to make. Do they want to increase sales? Is their goal to increase market share? Do they want to enter a new market? Maybe they want to increase engagement to create more loyal customers who have a higher lifetime value.
一旦你了解了业务部门想要产生的影响,你就能更有效地帮助他们实现目标。对于领域驱动设计 (DDD) 来说,重要的是,由于你了解业务部门想要实现的目标,你将能够在知识交流环节提出更好的问题。
Once you understand the impact the business is trying to make you can play a more effective role in helping them to achieve it. Significantly for DDD, you will be able to ask better questions during knowledge-crunching sessions since you know what the business wants to achieve.
令人惊讶的是,影响力图谱绘制其实是一种非常非正式的技术。你只需创建类似思维导图的图表,突出关键的业务信息即可。你与业务部门合作,就像知识整合一样,这是一种协作活动,有助于构建产品的共同愿景。图 2.3展示了一个影响力图示例,展示了一家电商公司希望实现的提升 25% 自行车销量的目标。
Surprisingly, impact mapping is a very informal technique. You simply create mind-map-like diagrams that accentuate key business information. You work with the business so that, like knowledge crunching, it is a collaborative exercise that helps to build up a shared vision for the product. Figure 2.3 shows an example impact map demonstrating an e-commerce company’s desired impact of selling 25% more bicycles.
图 2.3影响图。
FIGURE 2.3 An impact map.
很明显,影响图从影响开始。在图 2.3中,正如所提到的,这就是多销售 25% 的自行车。与影响直接相关的是参与者——那些可以为实现预期影响做出贡献的人。在图 2.3中,这些参与者是开发人员和数据科学家。参与者的子节点是参与者可以提供帮助的方式。在图 2.3中,开发人员可以帮助创造业务影响的一种方法是提高网站的性能,以便人们更有可能进行购买。最后,层次结构的最后一级显示了可以执行的实际任务。您可以在图 2.3中看到,开发人员可以加快网站速度的一种方法是删除速度慢的框架。
An impact map, rather obviously, starts with the impact. In Figure 2.3, this is to sell 25% more bicycles, as mentioned. Directly connected to the impact are the actors—the people who can contribute to making the desired impact. In Figure 2.3, that would be developers and data scientists. Child nodes of the actors are the ways in which the actors can help. In Figure 2.3, one way the developers can help to create the business impact is to improve the performance of the website so that people are more likely to make a purchase. Finally, the last level of the hierarchy shows the actual tasks that can be carried out. You can see in Figure 2.3 that one way the developers may be able to make the website faster is to remove slow frameworks.
在许多软件项目中,开发人员只能获得影响图的底层——业务部门认为他们需要什么,以及他们认为开发人员应该如何实现这些目标。然而,通过影响图,你可以解开他们的假设,找出他们真正想要实现的目标。然后您可以利用您的技术专长来提出他们从未想到过的更好的替代方案。
On many software projects the developers only get the lower tiers of an impact map—what the business thinks they need and how they think the developers should achieve it. With an impact map, though, you can unwind their assumptions and find out what they really want to achieve. And then you can use your technical expertise to suggest superior alternatives that they would never have thought of.
一些 DDD 从业者对影响图的评价很高,无论将其与 DDD 结合使用还是单独使用。我们强烈建议您浏览网站 ( http://www.impactmapping.org/ ) 或阅读 Gojko Adzic 所著的《影响图》一书,深入了解影响图。
Some DDD practitioners rate impact mapping very highly, both when applied with DDD or in isolation. You are highly encouraged to investigate impact mapping by browsing the website (http://www.impactmapping.org/) or picking up a copy of the book: “Impact Mapping,” by Gojko Adzic.
商业模式包含大量有用的领域信息,并强调了企业的基本目标。遗憾的是,很少有开发人员花时间去了解雇主的商业模式,甚至去理解商业模式的真正含义。
A business model contains lots of useful domain information and accentuates the fundamental goals of a business. Unfortunately, very few developers take the time to understand the business model of their employers or even to understand what business models really are.
了解一家公司商业模式的最佳方法之一是使用商业模式画布将其可视化;亚历山大·奥斯特瓦德 (Alexander Osterwalder) 和伊夫·皮尼厄 (Yves Pigneur) 在他们颇具影响力的著作《商业模式新生代》(Business Model Generation) 中提出了一种可视化技术,这本书非常值得开发人员阅读,也非常容易理解。商业模式画布非常有用,因为它将商业模式分解为九个构建模块,如图 2.4所示,该图展示了一家在线体育器材供应商的商业模式画布示例。
One of the best ways to learn about a company’s business model is to visualize it using a Business Model Canvas; a visualization technique introduced by Alexander Osterwalder and Yves Pigneur in their influential book, “Business Model Generation” highly recommended and very accessible reading for developers. A Business Model Canvas is extremely useful because it breaks down a business model into nine building blocks, as shown in Figure 2.4 which illustrates an example Business Model Canvas for an online sports equipment provider.
图 2.4商业模式画布。
FIGURE 2.4 A Business Model Canvas.
了解商业模式的九大基石,就能让你了解哪些因素对企业至关重要。关键信息包括:商业模式如何盈利、最重要的资产是什么,以及至关重要的目标客户。下文将逐一介绍商业模式的各个组成部分。欲了解更多信息,《商业模式新生代》一书是理想的学习资源。
Understanding the nine building blocks of a business model tells you what is important to the business. Key information like: how it makes money, what its most important assets are, and crucially its target customers. Each of the sections of a business model is introduced below. For more information, the “Business Model Generation” book is the ideal learning resource.
掌握商业模式画布提供的信息后,您将能够向领域专家提出有意义的问题,并帮助推动业务发展,而不仅仅是技术实现。寻找和理解雇主的商业模式,哪怕只是小小的努力,也是值得的。
Armed with the information presented by a Business Model Canvas you will be empowered to ask meaningful questions of domain experts and help to drive the evolution of the business—not just the technical implementation. The small effort of finding and understanding your employer’s business model is well worth it.
BDD 的创始人 Dan North 发表了一种提升领域知识的方法,名为“深思熟虑的发现”(http://dannorth.net/2010/08/30/introducing-deliberate-discovery/)。与其在规划和需求收集阶段(例如规划扑克和故事创作活动)专注于敏捷方法的框架,不如花时间学习问题领域中你不了解的部分。Dan 指出:“无知是提高吞吐量的最大障碍。” 因此,更丰富的领域知识将提升你的建模工作。
Dan North, the creator of BDD, has published a method for improving domain knowledge called deliberate discovery (http://dannorth.net/2010/08/30/introducing-deliberate-discovery/). Instead of focusing on the framework of agile methodologies during planning and requirement gathering stages, such as the activities of planning poker and story creation, you should devote time to learning about areas of the problem domain that you are ignorant about. Dan states that “Ignorance is the single greatest impediment to throughput.” Therefore a greater amount of domain knowledge will improve your modeling efforts.
在项目开始时,团队应该齐心协力,找出问题领域中他们最不了解的部分,以确保在知识消化会议期间解决这些问题。团队应该利用知识消化会议来识别未知的未知部分,即他们尚未发现的领域部分。这项工作应该由领域专家和利益相关者主导,他们可以帮助团队专注于重要领域,而不是简单地消化知识。整个问题领域。这将使团队能够识别领域知识的差距并快速处理它们。
At the start of a project teams should make a concerted effort to identify areas of the problem domain that they are most ignorant of to ensure that these are tackled during knowledge-crunching sessions. Teams should use knowledge-crunching sessions to identify the unknown unknowns, the parts of the domain that they have not yet discovered. This should be led by the domain experts and stakeholder who can help the teams focus on areas of importance and not simply crunching the entire problem domain. This will enable teams to identify the gaps in domain knowledge and deal with them in a rapid manner.
领域驱动设计的创始人 Eric Evans 创建了一份名为“模型探索漩涡”(http://domainlanguage.com/ddd/whirlpool/)的草稿文档。该文档介绍了一种建模和知识挖掘方法,可以作为其他敏捷方法的补充,并可在应用程序开发生命周期的任何时候调用。它并非作为一种建模方法,而是用于在模型创建过程中遇到问题时使用。诸如与业务部门沟通中断、解决方案设计过于复杂,或者完全缺乏领域知识等迹象,都会促使人们进入“模型探索漩涡”中定义的流程,并挖掘领域知识。
Eric Evans, the creator of Domain-Driven Design, has created a draft document named the Model Exploration Whirlpool (http://domainlanguage.com/ddd/whirlpool/). This document presents a method of modeling and knowledge crunching that can complement other agile methodologies and be called upon at any time of need throughout the lifetime of application development. It is used not as a modeling methodology but rather for when problems are encountered during the creation of a model. Telltale signs such as breakdowns in communication with the business and overly complex solution designs or when there is a complete lack of domain knowledge are catalysts to jump into the process defined in the Model Exploration Whirlpool and crunch domain knowledge.
漩涡浴包含以下活动:
The whirlpool contains the following activities:
场景探索
领域专家会描述团队在问题领域中担心或遇到困难的场景。场景是指一系列对领域专家来说很重要的步骤或流程,是应用程序的核心,并且属于项目范围之内。领域专家使用团队能够理解的具体示例解释了场景后,团队会将场景绘制成图,就像在开放空间中以可视化的方式进行事件风暴一样。
Scenario Exploring
A domain expert describes a scenario that the team is worried about or having difficulty with in the problem domain. A scenario is a sequence of steps or processes that is important to the domain expert, is core to the application, and that is within the scope of the project. Once the domain expert has explained the scenario using concrete examples the team understands, the group then maps the scenario, like event storming in a visual manner in an open space.
造型
在运行场景的同时,团队开始检查当前模型并评估其对于解决领域专家所表达的场景的有用性。
Modeling
At the same time of running through a scenario, the team starts to examine the current model and assesses its usefulness for solving the scenario expressed by the domain expert.
挑战模型
一旦团队修改了模型或创建了新模型,他们就会用领域专家的进一步场景来挑战它,以证明其有用性。
Challenging the Model
Once the team has amended the model or created a new model they then challenge it with further scenarios from the domain expert to prove its usefulness.
收获和记录
有助于演示模型的重要场景应该记录在文档中。关键场景将构成参考场景,用于演示模型如何解决问题域中的关键问题。业务场景的变化频率低于模型,因此,收集一些重要的场景作为模型更改时的参考会很有帮助。但是,不要试图记录每个设计决策和每个模型;有些想法应该留在绘图板上。
Harvesting and Documenting
Significant scenarios that help demonstrate the model should be captured in documentation. Key scenarios will form the reference scenarios, which will demonstrate how the model solves key problems within the problem domain. Business scenarios will change less often than the model so it is useful to have a collection of important ones as a reference for whenever you are changing the model. However, don’t try and capture every design decision and every model; some ideas should be left at the drawing board.
代码探测
当对问题领域的洞察被解锁并且设计取得突破时,技术团队应该在代码中证明它以确保它可以实现。
Code Probing
When insight into the problem domain is unlocked and a design breakthrough occurs the technical team should prove it in code to ensure that it can be implemented.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
重要的是要理解,问题的各个部分并非都相同。应用程序的某些部分比其他部分更重要。某些部分需要比其他部分更多的关注和投入才能使应用程序取得成功。在与领域专家进行知识交流时,重要的是减少不重要内容的干扰,以便您能够专注于重要的事情。模型驱动设计很难,应该只保留到对系统成功至关重要的领域。本章介绍如何揭示系统中最重要的区域,以及如何通过使用提炼来专注于这些区域。有了关注点的知识,您就可以深入建模核心内容,并专注于那些会产生影响的内容。
It’s important to understand that not all parts of a problem are equal. Some parts of the application are more important than others. Some parts need more attention and investment than others to make the application a success. During knowledge crunching with domain experts, it’s important to reduce the noise of what’s unimportant to enable you to focus on what is important. Model-Driven Design is hard and should only be reserved to the areas of your systems that are vital to its success. This chapter covers how you can reveal the most important areas of a system and how by using distillation you can focus on those areas. With the knowledge of where to focus you can deeply model what is core, and focus on what will make a difference.
为复杂问题领域构建的大型系统将由各种组件和子系统组成,所有这些对于确保整个系统的正常运行都至关重要。然而,解决方案的某些部分会比其他部分更有价值。因此,必须将精力和注意力集中在对业务至关重要的领域。你不可能也不需要在整个系统中平均分配精力和质量。试图追求平等会导致你失去对真正重要领域的关注,而这正是做好事的关键所在。
Large systems built for complex problem domains will be made up of a combination of components and sub systems, all of which are essential to ensure the entire systems works. However some parts of the solution will be more valuable than others. It is essential therefore to focus effort and attention on the areas that are important to the business. You cannot equally spread effort and quality throughout the entire system, nor do you need to. Trying to strive for equality will result in a loss of focus on the real area that is essential to get right.
为了理解问题领域中最有价值的部分,我们需要对其进行提炼,以揭示核心内容。通过分解大型问题领域,我们可以更有效地为不同领域提供资源,确保最优秀的开发人员能够专注于对业务最重要的领域,而不是那些技术挑战性更高或需要使用新基础设施框架的领域。从大型问题领域中提炼出的子领域,也是我们构建解决方案方式的参考。
In order to understand the most valuable areas of a problem domain we need to distill it to reveal what is core. By breaking up the large problem domain we can more effectively resource the different areas to ensure the most talented developers are working in the areas of most importance to the business rather than the areas that may be more technically challenging or that utilize new frameworks of infrastructure. The subdomains distilled from the large problem domain are also an input to the way we will architect the solution.
要知道应该把精力集中在哪里,你首先需要了解是什么让这个应用程序值得设计。你需要了解公司的商业战略,以及你正在开发的软件为何能够实现这一战略。值得思考的是,为什么要开发定制软件,而不是选择现成的商业产品。构建应用程序对业务有何影响?它如何融入公司的战略?为什么要内部开发而不是外包?软件的某些部分是否能为公司带来竞争优势?
To know where to focus effort you need to understand what makes the application worth designing in the first place. You need to understand the business strategy and why the existence of the software you are creating will enable it. It is worth asking why the custom software is being written rather than opting for a commercial off-the-shelf product. How will building an application make a difference to the business? How does it fit within the strategy of the company? Why is it being built in-house rather than being outsourced? Does part of the software give the business a competitive edge?
警惕业务用户对现有软件增强功能的要求,因为他们通常会根据当前系统的限制而不是他们真正的需求提出要求。问问自己,你有多少次与用户沟通,真正了解了需求背后的动机。你是否理解了需求背后的原因?一旦你分享并了解了客户的真正需求,你通常可以提出更好的解决方案。当你这样与客户沟通,紧接着是经典台词:“哦,真的吗?我不知道你还能这样做!”时,客户通常会感到惊讶。记住:你是推动者。不要盲目遵循用户的需求。业务用户可能无法编写有效的功能或有效地表达目标。你必须分享和理解潜在的愿景,并了解企业想要实现的目标,这样你才能提供真正的商业价值。
Be wary of business users asking for enhancements to existing software, because they will often give you requirements that are based on the constraints of the current systems rather than what they really desire. Ask yourself how often you have engaged with a user to really find the motivation behind a requirement. Have you understood the why behind the what? Once you share and understand the real needs of a customer, you can often present a better solution. Customers are usually surprised when you engage them like this, quickly followed by the classic line: “Oh, really? I didn’t know you could do that!” Remember: You are the enabler. Don’t blindly follow the user’s requirements. Business users may not be able to write effective features or effectively express goals. You must share and understand the underlying vision and be aware of what the business is trying to achieve so you can offer real business value.
在开始任何产品合作之前,务必要求提供项目概述。在任何大型组织中,项目启动过程早在开发团队参与之前就已开始。公司通常会准备一份简短的论文,解释为什么公司希望为这个项目开发软件而不是其他项目。这份论文往往是核心领域的关键所在。它论证了为什么开发这个软件是一个好主意;仔细研究这份论文,找出重点。将要点写在白板上,让团队所有成员都明白他们为什么要开发这个软件。
Before embarking on any product engagement, always ask for a project overview. In any large organization, the process of project inception starts long before the development team gets involved. Often there will be a small paper on why the company wants to build the software for this initiative over another. This paper often holds the key to the core domain. The paper is a justification on why writing the software is a good idea; study this and pick out what is important. Make it explicit by writing the salient points on the whiteboard so all on the team understand why they are writing the software.
在项目开始时,可以创建一个领域愿景声明,明确阐述软件成功的核心要素、业务目标以及价值所在。这条信息应该与团队共享,甚至可以贴在办公室的墙上,提醒大家编写这款软件的初衷。
A domain vision statement can be created at the start of a project to explicitly capture what is central to the success of the software, what the business goal is, and where the value is. This message should be shared with the team and even stick it up on a wall in the office as a reminder to why the software is being written.
大型问题域可以划分为子域,以管理复杂性并将重要部分与系统其他部分分离。图 3.1展示了在屠宰领域中,一头猪被分割成多个部位,就像问题空间一样。了解系统的子域有助于您分解问题空间。子域是抽象概念;不要将子域与公司的组织结构混淆。子域代表能力领域,定义业务流程,并代表系统的功能。
Large problem domains can be partitioned into subdomains to manage complexity and to separate the important parts from the rest of the system. Figure 3.1 shows how in the domain of butchery, a pig is divided into cuts much like a problem space. Understanding the subdomains of your system enables you to break down the problem space. Subdomains are abstract concepts; don’t get subdomains confused with the organizational structure of a company. Subdomains represent areas of capability, define business processes, and represent the functionality of a system.
图 3.1猪的切块。
FIGURE 3.1 Cuts of a pig.
在分析问题空间时,尽量不要在谈话中提及技术问题。安全是一个技术问题,除非你的问题空间就是安全。审计跟踪和日志记录也是基础设施方面的问题。因此,首先要关注的是领域本身。
Try not to bring technical concerns into conversation when you are analyzing the problem space. Security is a technical concern unless your problem space is security. Audit trails and logging are also infrastructural concerns. Instead, keep focused on the domain first and foremost.
通过提炼问题域,您可以通过分而治之来降低复杂性。可以在子域的上下文中创建和理解较小的模型。这样就不需要单个大型模型来表示整个问题域。许多定义的子域可能对任何企业业务软件都是通用的,例如报告和通知需求。这些子域不定义应用程序,因此被称为通用域。那些将您公司的独特产品与竞争对手的产品区分开来并定义其在市场上具有竞争优势的领域被称为核心域。核心域是您自己编写此软件的原因。构成大型应用程序的其余子域被称为支持域,它们是核心域和系统的推动者。
By distilling the problem domain you reduce complexity by dividing and conquering the problem. Smaller models can be created and understood within the context of a subdomain. This removes the need for a single large model to represent the entire problem domain. Many of these subdomains that are defined may be generic to any enterprise business software, such as reporting and notification needs. These subdomains, which do not define the application, are referred to as generic domains. The areas that distinguish your company’s unique product offering from a rival’s and define what gives it a competitive edge in the market are known as your core domains. The core domains are the reason why you are writing this software yourself. The remainder of the subdomains that make up large-scale applications are known as supporting domains, which are enablers for the core domain and the system.
以在线拍卖网站的领域模型为例,如图 3.2所示。这个庞大的整体系统由许多不同的组件组成。有些部分在任何在线系统中都能找到,但有些部分则是该领域和特定业务所独有的。
Take the domain model of an online auction site, as shown in Figure 3.2. There are many different components that make up the large overall system. Some parts will be found in any online system, but some will be unique to the domain and specific business.
图 3.2在线拍卖网站的域名。
FIGURE 3.2 The domain of an online auction site.
图 3.3展示了如何将大型问题域划分为子域。会员系统负责处理会员的注册、偏好设置和详细信息。卖家系统负责处理卖家活动的所有流程和行为。拍卖系统负责管理拍卖时间和处理竞价活动。商品列表系统负责拍卖网站上可供出售的商品目录。最后,争议解决系统负责处理会员和卖家之间的争议。
Figure 3.3 shows how the large problem domain is partitioned into subdomains. Membership represents the area of the systems that deals with the registrations, preferences, and details of members. The seller partition represents all the processes and behaviors that deal with seller activities. Auction is the area of the problem domain that deals with managing the timing of auctions and dealing with bid activity. Listings are the catalogues of items that are available on the auction site. Finally, the dispute resolution domain deals with disputes between members and sellers.
图 3.3在线拍卖网站的域名被提炼为子域名。
FIGURE 3.3 The domain of an online auction site distilled into subdomains.
与领域专家进行交流后,知识的提炼应该能够揭示出您即将创建的应用程序的独特之处和重要性。您可以将子域划分为核心域、通用域和支持域,如图3.4所示。
The distillation of knowledge after sessions with domain experts should reveal what’s unique and important about the application you are about to create. You can separate the subdomains into core, generic, and supporting domains, as shown in Figure 3.4.
图 3.4在线拍卖网站的提炼域分为核心域、通用域和支持域。
FIGURE 3.4 The distilled domain of an online auction site partitioned into core, generic, and supporting domains.
在图 3.4中,您可以看到在线拍卖网站的核心域是卖家域和拍卖域。卖家域包含卖家的评级和确定卖家费用的域逻辑。拍卖核心域是运行拍卖和处理竞价的机制。这两个领域对于拍卖网站的成功都至关重要。会员域和列表域通过为竞标者提供创建帐户的机会来支持核心域。并查找待售商品。争议解决领域是通用的,因为它可以使用现成的商业软件包来提供服务;在这种情况下,它仅仅是一个处理客户争议案件的滴答系统。
In figure 3.4 you can see that the core domains of the online auction site are the seller and the auction. The seller domain contains the ratings for a seller and the domain logic for determining seller fees. The auction core domain is the mechanism for running an auction and handling bids. Both of these areas are vital for the success of the auction site. The membership and listing domains support the core domains by providing bidders the opportunity to create accounts and find items for sale. The dispute resolution domain is generic in that it can be served using a commercial off-the-shelf package; in this scenario it is merely a ticking system to handle customer dispute cases.
要知道在哪里投入最多的精力和质量,关键在于了解核心领域在哪里,因为这些领域是软件成功的关键。这些知识是通过与领域专家合作进行知识梳理会议提炼出来的,旨在了解正在开发的产品的最重要方面。
To know where to invest the most effort and quality, it’s crucial to understand where the core domains are, because these are key to making the software successful. This knowledge is distilled from knowledge-crunching sessions working in collaboration with domain experts to understand the most important aspect of the product under development.
要了解你的企业要求你开发的产品的核心是什么,你需要问自己一些问题。产品的哪些部分能够使其成功?为什么这些系统部分如此重要?为什么它们不能直接购买?换句话说,是什么让你的系统值得构建?
To understand what’s core to the product that your business is asking you to develop, you need to ask yourself some questions. What are the parts of the product that will make it a success? Why are these parts of the system important? And why can’t they be bought off the shelf? In other words, what makes your system worth building?
系统的核心部分代表了贵公司通过交付该软件所能获得的基本竞争优势。核心部分并不总是显而易见的。
The core parts of the system represent the fundamental competitive advantage that your company can gain through the delivery of this software. What’s core is not always obvious.
如果说通用领域应该被引入,并且几乎不进行开发,那么核心领域则恰恰相反。核心领域需要你最好的开发人员——或者说,你的“突击队员”。核心领域或许不会占据公司技术的最大份额,但它们需要的投资最多。
If the generic domains should be brought in and have little development, the core domain is the polar opposite. The core domains require your best developers—your commandos, if you will. The core domains may not make up the lion’s share of your company’s technology, but they require the most investment.
核心是什么当然会随着时间而变化。如果你成功了,竞争对手就会效仿,所以核心领域必须不断发展,才能使你的业务脱颖而出,保持领先地位。开发团队必须考虑到这一点,并确保它与软件和业务的价值相一致。
What is core certainly changes over time. If you are successful, competitors mimic, so the core domain must evolve to set your business apart from the rest and keep it ahead of the game. It’s vital that the development team take this on board and ensure it is in synergy with the values of the software and the business.
无论是开发团队还是业务团队,在为复杂核心领域编写软件时,都需要在思维方式上做出根本性的转变,那就是专注于产品本身,而不是将其视为一个独立的项目。通常,商业产品的软件开发永远不会结束;相反,开发中的产品会经历功能增强的阶段。软件会持续投入,直到它不再为业务提供价值,或者无法通过进一步的修改来丰富。
One of the fundamental shifts in mentality required for writing software for complex core domains, from both the development team and the business, is to focus on the product rather than view it as a standalone project. Often, the development of software for a business product is never finished; instead, the product under development will live through periods of feature enhancements. The software is invested in until it is no longer providing value for the business or it can’t be enriched with further modifications.
您的产品是一个不断发展变化、功能增强和调整的体系。正如开发人员不断迭代一样,业务也在不断更新。一个好的想法会随着时间的推移而不断改进和充实,最终变得更好。了解您正在开发的产品的价值以及它能为公司带来的投资回报率 (ROI)。与您的业务赞助商讨论产品的未来发展,这有助于您集中精力进行编码工作;了解对他们来说什么是重要的。
Your product is an evolving succession of feature enhancements and tweaks. Just as developers iterate, so, too, does the business. A good idea becomes better after it is improved upon and fleshed out over time. Understand the value of the product you are working on and what return on investment (ROI) it brings to the company. Talk to your business sponsors about the future of the product to help focus your coding efforts; know what is important to them.
很多时候,企业核心领域的软件并不被视为需要精心呵护的产品。相反,商业软件为了快速上市而牺牲了质量和长期投入。人们过于注重项目本身和迫在眉睫的截止日期,而忽略了对产品未来的投资。这导致代码库难以维护和增强,并陷入了“大泥球”(BBoM)的架构模式,正如第一章“什么是领域驱动设计?”中所讨论的那样。
All too often, software for the core domain of a business isn’t viewed as a product that requires care and attention. Instead, business software sacrifices quality and long-term investment for speed to market. Too much emphasis is placed on thinking about the project and looming deadlines, rather than investing in the product for the future. This results in a codebase that is hard to maintain and enhance, and falls into the architectural pattern of the Big Ball of Mud (BBoM), as discussed in Chapter 1, “What Is Domain-Driven Design?”
然而,另一方面,发布日期的延长往往是不可避免的,因为其他商业利益也取决于软件的发布日期。解决这一困境的方法是精简功能范围,以保持高质量并按时完成交付。为了做到这一点,您必须理解并分享软件预期实现的愿景和最终目标。这种理解使您能够只包含产品中最重要的功能,并确保其能够提供企业期望的价值。
The flip side, however, is a prolonged release date, which is often nonnegotiable if other business interests depend on the launch date of the software. The solution to this quandary is to look to descope features to keep quality high and the deadline on track. To be in a position to do this, you must understand and share the vision and ultimate goal that the software is expected to meet. This understanding enables you to include only the most important features of the product and ensure that it delivers the value the business expects.
通用域名是许多大型业务系统都拥有的子域名。例如,电子邮件发送服务、帐户软件包或报表套件。这些子域名并非业务的核心,但业务运营离不开它们。由于这些子域名并非核心,也不会给您带来竞争优势,因此花费大量精力或投资来构建它们并不合理。您可以考虑购买通用域名的软件。或者,您可以聘请初级开发人员来构建这些系统,从而腾出更多经验丰富的资源来处理业务的核心工作。
A generic domain is a subdomain that many large business systems have. An example of a generic domain is an e-mail sending service, an accounts package, or a report suite. These subdomains aren’t core to the business, but the business can’t operate without them. Because these subdomains aren’t core and won’t give you a competitive edge, it doesn’t make sense to spend a lot of effort or investment in building them. Instead, look to buy in software for generic domains. Alternatively, use junior developers to build these systems, freeing up more experienced resources to work on what’s core to your business.
然而,需要注意的是,像 Groupon 或 Wowcher 这样以沟通和限时优惠定向邮件为特色的企业,其核心领域可能就是复杂的电子邮件/CRM 系统。一家企业的核心功能,对另一家企业来说可能也是通用的。
Note, however, that a business defined by communication and targeted e-mails on limited-time offers, like a Groupon or a Wowcher, could have its core domain as a sophisticated e-mail/CRM system. What is core to one business may well be generic to another.
系统中剩余的子域被定义为支持域。这些子域虽然不定义系统功能,但有助于支持核心域。例如,亚马逊的支持域是允许客户浏览产品目录的功能。亚马逊的产品浏览功能并不能将其定义为一家公司,也与其他任何电商网站并无太大区别,但它确实支持跟踪用户旅程以提供推荐引擎。
The remaining subdomains in the system are defined as the supporting domains. These are subdomains that, although not defining what your system does, help to support your core domains. For example, Amazon’s supporting domains would be the functionality that enables a customer to browse a catalog for products. Amazon’s product-browsing functionality doesn’t define it as a company, and neither is it that different from any other e-commerce site, but it does support the tracking of user journeys to feed a recommendations engine.
与通用域一样,如果可能的话,您应该考虑购买现成的解决方案。如果做不到,请不要在这些系统上投入大量资金;它们需要工作,但不需要您长时间关注。需要注意的是,您并不总是需要为支持域实施技术解决方案。也许手动流程可以满足业务需求,而开发人员可以专注于核心域。
As with the generic domains, if possible, you should look to buy off-the-shelf solutions. Failing that, do not invest heavily in these systems; they need to work but do not require your prolonged attention. It’s important to note that you may not always need to implement a technical solution to a supporting domain. Perhaps a manual process could meet the needs of the business while developers focus on the core domain.
在每个子域内都可以创建一个模型。图 3.5展示了如何将在线拍卖网站划分为两个物理应用程序。争议域由现成的软件包实现,而核心域和支持域则使用自定义 Web 应用程序构建。
Within each subdomain a model can be created. Figure 3.5 shows how the online auction site has been divided into two physical applications. The dispute domain has been fulfilled by an off-the-shelf package while the core and supporting domains have been built using a custom web application.
图 3.5解决方案如何映射到拍卖系统的子域。
FIGURE 3.5 How a solution maps to the subdomains of the auction system.
每个子域中都会有一个模型,用于表示与系统该区域相关的领域逻辑和业务规则。并非所有这些模型的质量都相同。了解构成系统的各个子域后,您可以相应地分配工作,并将 DDD 的模型驱动设计模式应用于最能受益的领域。
Within each subdomain there will be a model that represents the domain logic and business rules that are relevant to that area of the system. Not all of these models will be of equal quality. With an understanding of the various subdomains that comprise your system you can apportion effort accordingly and apply the model-driven design patterns of DDD to the areas that will benefit most.
不要浪费时间和精力重构所有代码——确保你的主要精力集中在核心领域。如果你最终得到的是支持领域和通用领域的代码,虽然能用,但却很“混乱”,那么别管它。好的就足够了。保留一些 BBoM 的小块代码是可以的,只要它们在明确定义的范围内。完美只是一种幻觉。完美应该只保留在核心部分。业务部门并不关心那些必需但并非系统关键、且不太可能长期投入的领域的代码质量。
Don’t waste time and effort on refactoring all of your code—ensure your primary focus is on the core domain. If you end up with working but “messy code” for supporting and generic domains then leave it alone. Good is good enough. Leaving small silos of BBoM is fine as long as they are within clearly defined boundaries. Perfection is an illusion. Perfection should be reserved for only what is core. The business doesn’t care about quality code for areas that are required but are not key to the system and which are unlikely to be invested in over time.
大泥球模式是最流行的软件架构模式。在经过长期演进的大型软件系统中,系统中很可能存在一些不完善的地方。如果应用程序中的某些区域与大泥球模式类似,那么最好的办法就是在这些区域周围设置边界,以防止泥球蔓延到应用程序的其他区域。图 3.6展示了一个应用程序的解决方案空间,该应用程序在旧版大泥球模式和新模型之间定义了明确的边界。可以使用防腐层来防止一个模型模糊地融入另一个模型。
The Big Ball of Mud pattern is the most popular software architectural pattern. In large-scale software systems that have evolved over time there are more than likely areas of the system that are not perfect. If you have areas of an application that resemble the BBoM pattern then the best thing to do is to put a boundary around them to prevent the mud spreading into new areas of the application. Figure 3.6 shows the solution space of an application that has defined explicit boundaries between the legacy BBoM and the new models. An anti-corruption layer can be used to prevent one model blurring into another.
图 3.6处理遗留问题。
FIGURE 3.6 Dealing with legacy.
在理想情况下,高质量的软件始终是您的首要任务;然而,务实也很重要。有时,新系统的核心领域可能是第一个进入市场的,有时企业可能无法判断某个特定想法是否会成功并成为其成功的核心。在这种情况下,企业希望快速学习,快速失败,而无需投入大量的前期精力。
In an ideal world, quality software would always be top of your agenda; however, it’s important to be pragmatic. Sometimes a new system’s core domain could be first to market, or sometimes a business may not be able to tell if a particular idea will be successful and become core to its success. In this instance, the business wants to learn quickly and fail fast without putting in a lot of up-front effort.
如果产品的第一个版本未被业务部门充分理解,那么它可能不会被精心设计。这很正常,因为业务部门不确定是否会长期投入,开发团队应该理解为什么业务部门更看重交付速度而不是灵活的设计。但是,如果产品成功了,并且长期投资软件是有价值的,那么就需要进行重构以支持后续的演进;否则,匆忙交付所积累的技术债务就会开始成为一个问题。
The first version of a product that is not well understood by the business may not be well crafted. This is fine, because the business is unsure if it will be invested in over time, and the development team should understand why the business wants speed of delivery over supple design. However, if the product is a success and there is value in a prolonged investment in the software, you need to refactor to support the evolution; otherwise, the technical debt racked up in the rush to deliver starts to become an issue.
在子域中开发模型时,请尝试独立构建它们,并考虑替换。使用清晰的边界,将它们与其他模型、遗留代码和第三方服务区分开。通过以替换而非重用为目标进行编码,您可以创建足够好的支持子域,而无需浪费精力去完善它们。将来,它们可以被现成的解决方案取代,或者可以根据业务需求的变化进行重写。
When developing models in subdomains try and build them in isolation with replacement in mind. Keep them separated from other models, legacy code, and third party services by using clean boundaries. By coding for replacement rather than reuse you can create good enough supporting subdomains without wasting effort on perfecting them. In the future they can be replaced by off-the-shelf solutions or can be rewritten as business needs change.
企业选择自行开发而非购买软件的原因有很多。如果您能够更便宜、更快速或更智能地完成开发,那么定制开发就是一个不错的选择。如果您发现您正在构建的软件都是通用的,或者正在支持企业中的其他应用程序,因此您如果没有核心领域,就不要尝试将 DDD 的所有实践和原则都应用到你的项目中。遵循 DDD 的战略模式仍然可以让你受益,但模型驱动设计的战术模式可能会徒劳无功。你将在第九章“团队初学领域驱动设计时常见问题”中了解更多关于何时应该以及何时不应该应用 DDD 模型驱动模式的信息。
There are many reasons that businesses build rather than buy software. If you can do it cheaper, faster, or smarter then it’s a good candidate for a custom build. If you find that the software you are building is all generic or is supporting other applications in your enterprise and therefore you have no core domain then don’t try and apply all of the practices and principles of DDD to your project. You can still benefit from following the strategic patterns of DDD but the Model-Driven Design tactical patterns could be wasted effort. You will learn more about when to and when not to apply the model-driven patterns of DDD in Chapter 9, “Common Problems for Teams Starting Out with Domain-Driven Design.”
本章内容是什么?
WHAT’S IN THIS CHAPTER?
凭借对问题域的深入理解和共同理解,以及对应用程序成功至关重要的核心领域的洞察,您现在可以专注于解决方案空间。然而,重要的是要将知识研讨期间生成的分析模型(即业务人员能够理解的模型)以代码形式实现。传统的软件流程将代码模型和分析模型分离,这会导致由于技术解决方案的新见解和约束,最终实现结果很少与蓝图相符。领域驱动设计 (DDD) 承认需要生成一个单一模型,作为业务人员能够理解的分析模型,并使用相同的术语和概念在代码中实现。
With a deep and shared understanding of the problem domain, along with insight into the core areas that are fundamental to the success of an application, you are now able to focus on the solution space. However, it is important to implement in code the analysis model that was produced during knowledge-crunching sessions; i.e., the model that the business understands. Traditional software processes keep the code model and analysis model separate, which leads to an implementation that rarely resembles the blueprint due to new insight and constraints of the technical solution. DDD acknowledges the need to produce a single model that serves as an analysis model for business people to understand and which is implemented using the same terminology and concepts in code.
这个过程被称为模型驱动设计,它高度依赖于通用语言 (Ubiquitous Language),将模型的技术实现与分析模型绑定,并在整个系统生命周期内保持同步。除了详细介绍模型驱动设计和通用语言之外,本章还涵盖了创建有效领域模型的模式以及应该使用模型驱动设计的场景。
This process is known as Model-Driven Design and is heavily dependent on Ubiquitous Language to tie the technical implementation of the model to the analysis model and keep them in sync throughout the lifetime of the system. As well as detailing Model-Driven Design and Ubiquitous Language, this chapter also covers patterns to create effective domain models and the scenarios where Model-Driven Design should be used.
领域模型,如图 4.1所示,是领域驱动设计 (DDD) 的核心。它最初是在开发团队和业务专家在知识研讨会议期间协作形成的分析模型。它代表了问题域的一种观点,而非现实,其设计仅用于满足业务用例的需求。它以团队使用的共享语言和团队绘制的图表来描述。当它表示为代码实现时,通过使用共享语言将其绑定到分析模型。它的实用性来自于它能够表示领域中复杂的逻辑和策略以解决业务用例。该模型仅包含与正在创建的应用程序上下文中解决问题相关的内容。它需要随着业务不断发展,以保持自身的实用性和有效性。
The domain model, as shown in Figure 4.1, is at the center of Domain-Driven Design (DDD). It is formed first as an analysis model through the collaboration between a development team and business experts during knowledge-crunching sessions. It represents a view, not the reality, of the problem domain designed only to meet the needs of business use cases. It is described in a shared language that the team speaks and the diagrams that the team sketches. When it is expressed as a code implementation, it is bound to the analysis model through the use of the shared language. Its usefulness comes from its ability to represent complex logic and polices in the domain to solve business use cases. The model contains only what is relevant to solve problems in the context of the application being created. It needs to constantly evolve with the business to keep itself useful and valid.
图 4.1领域模型的作用。
FIGURE 4.1 The role of a domain model.
领域代表您正在处理的问题领域。它是情况的确切现实。另一方面,领域模型是问题领域的抽象,以代码实现的形式表示,它代表问题的视图,而不是现实。图 4.2突出显示了这种差异。领域模型的用处在于它能够表示领域中复杂的逻辑和策略以解决业务问题,而不是它如何很好地反映现实。它也存在于一个更抽象的空间中:团队使用的语言和绘制的图表。该模型建立在开发团队和业务团队的协作之上。专家。该模型仅包含与正在创建的应用程序上下文中解决问题相关的内容。它需要随着业务不断发展,以保持其实用性和有效性。领域模型的存在只是为了帮助我们解决问题;为了有效,它需要清晰且不受技术复杂性的影响。这样,业务团队和开发团队就可以协作进行设计。
The domain represents the problem area you are working within. It is the firm reality of the situation. The domain model, on the other hand, is an abstraction of the problem domain, expressed as a code implementation that represents a view, not the reality, of the problem. This difference is highlighted in Figure 4.2. The usefulness of the domain model comes in its ability to represent complex logic and polices in the domain to solve business problems and not how well it reflects reality. It also exists in a more abstract space: in the language the team speaks and the diagrams it sketches. The model is built from the collaboration between the development team and the business experts. The model contains only what is relevant to solve problems in the context of the application being created. It needs to constantly evolve with the business to keep itself useful and valid. The domain model only exists to help us solve problems; in order to be effective it needs to have clarity and be free of technical complexities. This way both the business and development teams can collaborate on its design.
图 4.2领域与领域模型。
FIGURE 4.2 The domain versus the domain model.
分析模型有时也称为业务模型,是描述系统模型的一系列工件的集合。这些工件可以是任何东西,从香烟包装草图到非正式的UML。分析模型的存在是为了帮助开发团队和业务用户理解问题领域;它不是技术实现的蓝图。
Also sometimes known as a business model, an analysis model is a collection of artifacts that describe the model of a system. These artifacts can be anything from cigarette packet sketches to informal UML. The analysis model exists to help both the development teams and business users to understand the problem domain; it is not a blueprint for the technical implementation.
DDD 并不主张移除分析模型。远非如此,因为描述系统的模型本身就蕴含着巨大的价值。相反,DDD 强调需要保持代码模型(即实现)与分析模型(即设计)紧密协同。这种协同是通过确保两个模型都得到描述并共享 UL(工作流框架)来实现的,如图 4.3所示。理想中的模型是一个在实现和设计中都有价值的单一模型。为了实现这一点,至关重要的是要使代码模型摆脱技术顾虑,专注于领域。反过来,拥有一个可实现的分析模型也很重要——它不能太抽象或太高级而无用。
DDD doesn’t advocate the removal of the analysis model. Far from it, because there is much value to be gained from a model that describes the system. Instead, DDD emphasizes the need to keep the code model, the implementation, in close synergy with the analysis model, the design. This synergy is achieved by ensuring both models are described and share the UL, as shown in Figure 4.3. The utopia is a single model that has value in both implementation and design. To achieve this, it is crucial to keep the code model clean of technical concerns and focused on the domain. In turn, it is important to have an analysis model that can be implemented—not too abstract or high level to be of any use.
图 4.3代码和分析模型之间的绑定。
FIGURE 4.3 The binding between the code and analysis model.
代码模型是分析模型的实现;它验证业务假设,并快速突出显示与分析模型不一致之处。如果在代码模型创建过程中发现问题,且逻辑似乎不符,开发团队应与领域专家合作解决这些问题。代码模型的更新会通过修改工作流程和策略(这些修改之前可能并未暴露问题)反映在分析模型中。同样,任何来自业务视角的变更都需要反映在代码模型中。代码和业务模型应保持协同。代码即模型;代码即真相。
The code model is the realization of the analysis model; it validates the assumptions of the business and quickly highlights any inconsistencies with the analysis model. If, during the creation of the code model, issues are found and logic doesn’t seem to fit, the development team should work with the domain experts to resolve these problems. This update to the code model is reflected in the analysis model by making changes to work flow and polices that may not have exposed issues before. Likewise, any changes from a business perspective need to be reflected in the code model. The code and business models are kept in synergy. The code is the model; the code is the truth.
模型驱动设计是将分析模型与代码实现模型绑定的过程,确保两者在演进过程中保持同步且有效。它是一个在实践中验证和证明模型的过程,因为如果无法实际实现,那么拥有一个复杂的模型也是毫无意义的。模型驱动设计与领域驱动设计 (DDD) 的不同之处在于,模型驱动设计专注于实现以及任何可能需要更改初始模型的约束,而领域驱动设计则专注于语言、协作和领域知识。两者相辅相成;模型驱动设计方法能够将领域知识和共享语言融入到软件模型中,该模型能够反映业务专家的语言和思维模型。这有利于协作,因为业务专家和软件开发人员能够基于各自有效的模型共同解决问题。从任一模型中获得的见解都可以共享,知识也会随之增长,从而更好地解决问题,并促进业务和开发团队之间更清晰的沟通。
Model-Driven Design is the process of binding an analysis model to a code implementation model, ensuring that both stay in sync and are useful during evolution. It is the process of validating and proving the model in practice, because it’s pointless to have an elaborate model if you can’t actually implement it. Model-Driven Design differs from DDD in that it is focused on implementation and any constraints that may require changes to an initial model, whereas DDD focuses on language, collaboration, and domain knowledge. The two complement each other; a Model-Driven Design approach enables domain knowledge and the shared language to be incorporated into a software model that mirrors the language and mental models of the business experts. This then supports collaboration because business experts and software developers are able to solve problems together as a result of their respective models being valid. Insights gained in either model are shared and knowledge is increased, leading to better problem solving and clearer communication between the business and development team.
过去,软件系统需求的获取被视为一项早在编码开始之前就已进行的活动。业务专家会与业务分析师沟通,业务分析师再与架构师沟通,架构师会根据问题领域的所有信息生成分析模型。之后,该分析模型会连同线框图和工作流程图一起交给开发人员,以便他们构建系统。
Historically, the capturing of requirements for software systems was seen as an activity that could occur long before coding was due to start. Business experts would talk to business analysts, who in turn would talk to architects, who would produce an analysis model based on all the information from the problem domain. This analysis model would then be handed over to the developers, along with wireframes and work flow diagrams, so they could build the system
当开发人员开始用代码实现分析模型时,他们常常会发现架构师生成的高级工件与系统构建的实际情况不匹配。然而,在这个阶段,开发人员通常缺乏与业务人员和架构师沟通的反馈回路,因此无法更新分析模型并采纳他们的意见。相反,开发人员偏离了分析模型,他们的实现常常会忽略重要的、描述性的领域术语和概念,而这些术语和概念本可以提供对领域的更深入的洞察和理解。
As developers start to implement the analysis model in code, they often find a mismatch between the high-level artifacts produced by architects and the reality of building the system. However, at this stage there is often no feedback loop for developers to talk to the business and architects, so the analysis model can be updated and their input enacted. Instead, the developers diverge from the analysis model, and their implementation often overlooks important and descriptive domain terms and concepts that would have provided deeper insight and understanding of the domain.
随着开发团队逐渐脱离分析模型,它的作用也越来越小。由于开发团队专注于抽象技术问题而非业务概念,对模型的关键洞察逐渐丧失。最终,工作完成了,但代码却没有反映原始分析模型的正确性。业务部门仍然认为原始分析模型是正确的,并且没有意识到代码模型内部的变更。
As the development team further evolves away from the analysis model, it becomes less and less useful. Crucial insight into the model is lost as the development team focuses on abstracting technical concerns instead of business concepts. In the end the job gets done, but the code bears no reflection to the original analysis model. The business still believes the original analysis models are correct and is unaware of the alterations within the code model.
图 4.4显示了如果开发团队不参与领域知识的处理,分析模型和代码模型会如何相互分歧。
Figure 4.4 shows how the analysis and code models can diverge from each other if the development team is not involved in domain knowledge crunching.
图 4.4前期设计的问题。
FIGURE 4.4 The problems with upfront design.
当代码库后续的增强难以实现时,问题就暴露出来了。困难在于业务专家和开发人员对业务的理解不同。代码与业务流程缺乏协同,并且缺乏丰富的领域知识。
The problem is revealed when later enhancements to the codebase are difficult to implement. The difficulties are due to the business experts and developers having different models of the business. The code doesn’t have a synergy with the business processes and is not rich in domain knowledge.
DDD 建议采用一种更具协作性的方法来捕获系统需求并理解现有工作流程。重点在于整个团队,业务专家和架构师(只要他们参与编程)将围绕问题空间展开讨论。讨论内容可以涵盖与相关系统相关的任何文档或遗留代码。协作式知识交流会议的理念是让开发人员、测试人员、业务分析师、架构师和业务专家作为一个统一的团队协同工作。这使得开发人员和测试人员能够了解领域术语背后的含义,并理解问题领域中的复杂逻辑。它还使业务专家能够体验所使用的建模技术。理解建模后,业务专家将能够与开发团队一起建模和验证设计。
DDD suggests a more collaborative method of capturing system requirements and understanding existing work flow. Emphasis is placed on the entire team, with business experts and architects (as long as they code) having discussions around the problem space. Discussions can include any documentation or legacy code that is related to the system in question. The idea behind the collaborative knowledge-crunching sessions is for the developers, testers, business analysts, architects, and business experts to work as a unified team. This enables the developers and testers to learn about the meaning behind domain terms, and understand complex logic in the problem area. It also enables business experts to experience the modeling techniques employed. With an understanding of modeling, business experts will themselves be able to model and validate designs with the development team.
信息共享使业务专家能够参与软件设计,并为开发团队提供更深入的领域洞察和理解。一段时间后,开发人员和业务专家将收集相关信息,构建问题领域的初始模型。我们将使用领域场景(即该领域的实际问题)来测试该初始模型,以验证其有效性。使用模型的术语和语言进行“大声建模”也有助于验证早期设计。
The sharing of information enables business experts to contribute to the software design, and provides a deeper insight and understanding of the domain to the development team. After a period of time, developers and business experts will discover the relevant information to build an initial model of a problem domain. This initial model is put to the test by using domain scenarios: real problems of the domain to validate its usefulness. Modeling out loud, using the terms and language of the model, can also help to validate early designs.
共同建模的重要方面在于开发团队能够持续获得来自业务专家的反馈。这有助于发现重要的概念,并帮助团队了解哪些概念不重要,可以从模型中剔除。会议中的突破体现为简单的抽象,这些抽象能够阐明复杂的领域概念,并最终构建更具表现力的模型。
The important aspect of modeling together is the constant feedback the development team gets from the business experts. This leads to the discovery of important concepts and allows the team to understand what is not important and can be excluded from the model. Breakthroughs in sessions are manifested as simple abstractions that clarify complex domain concepts and lead to a more expressive model.
然后,该模型以代码形式表达,团队和业务专家可以通过软件的早期版本快速获得反馈。反馈反过来又能激发更深入的洞察,这可以反映在代码和分析模型中,如图4.5所示。
The model is then expressed in code and the team, along with business experts, can gain fast feedback with early versions of software. Feedback in turn fuels deeper insight, which can be reflected in the code and analysis models, as highlighted in Figure 4.5.
图 4.5代码模型和分析模型保持协同。
FIGURE 4.5 The code model and the analysis model are kept in synergy.
在每次迭代中,开发团队成员可能会发现模型中一些他们认为有用且可以解决问题但在实施过程中必须修改的部分。这些知识会被反馈给业务专家,以便他们澄清问题,并完善他们对问题领域的理解。在这个过程中,代码模型和分析模型是一体的,其中一个模型的变更会导致另一个模型的变更。
During each iteration, the development team members may come across parts of the model that they thought were useful and could solve a problem but during implementation had to change. This knowledge is fed back to the business experts for clarification and to refine their own understanding of the problem domain. In this process, the code model and analysis model are one, and a change in one will result in a change to the other.
图 4.6显示了分析和代码模型在产品创建过程中如何协同作用并作为一个整体发展。
Figure 4.6 shows how the analysis and code model are in synergy and evolve as one during the creation of a product.
图 4.6团队建模。
FIGURE 4.6 Team modeling.
遵循领域驱动设计 (DDD) 理念的真正价值在于开发人员和领域专家的协作,从而更好地理解领域。编写的代码只是该过程的一部分,尽管它很重要。为了加深理解,团队需要有效沟通。正是通用语言 (UL) 的诞生,使得更深入的理解在代码重写和替换后依然有效。
The true value of following the Domain-Driven Design (DDD) philosophy is in the collaboration of developers and domain experts to produce a better understanding of the domain. The code that is written is just an artifact of that process, albeit an important one. To reach a better understanding, teams need to communicate effectively. It is the creation of the ubiquitous language (UL) that enables a deeper understanding that will live on after code is rewritten and replaced.
UL 使团队能够轻松地组织思维模型和代码模型。由于它为团队带来了共同的理解,因此其含义清晰明确。UL 还能确保含义的清晰度和一致性。语言最终以代码表达,但语音、草图和文档对于创建语言也至关重要。语言会随着新的见解和更丰富的知识不断探索、验证和完善。
A UL enables teams to organize both the mental and the code model with ease. It achieves an unambiguous meaning because of the shared understanding that it brings to the teams. A UL also provides clarity and consistency in meaning. The language is ultimately expressed in code, but speech, sketch, and documentation are also important for creating the language. The language is constantly explored, verified, and refined with new insights and greater knowledge.
创建 UL 的实用性远不止应用于当前正在开发的产品。它有助于明确定义业务内容,更深入地洞察业务流程和逻辑,并改善业务沟通。
The usefulness of creating a UL has an impact that goes beyond its application to the current product under development. It helps define explicitly what the business does, it reveals deeper insights into the process and logic of the business, and it improves business communication.
我最近和妻子一起去买窗帘。褶皱、垂长、衬布——这些术语在窗帘制作领域都有着特定的意义。店里的员工可能会花几个小时来描述他们想要什么,但这可能会导致含义模糊。不过,由于员工们使用的是窗帘店领域的术语,所以对话会保持简短精炼,每个懂这个领域的人都能理解这些术语的含义。
I recently went curtain shopping with my wife. Pleated, hang length, interlining—these were all terms that meant something specific in the domain of curtain makers. Employees in the shop could spend hours describing what they wanted, but that could lead to ambiguity in meaning. But because the employees use terms in the domain of the curtain shop, conversations are kept short and concise, and everybody who understands the domain understands their meanings.
木匠、金融交易员、军人以及几乎所有你能想到的领域都是如此。每个领域都有各自独特的术语和概念。秘密语言能够让复杂的主题以简洁明了的对话形式进行讨论,而无需令人困惑的胡言乱语。对于开发团队来说,理解并协作使用这种被称为“通用语言”(UL)的语言至关重要。UL 的术语和概念用于与团队成员(包括领域专家)沟通。它们也用于命名代码库中的类、方法和命名空间。
It’s the same with carpenters, financial traders, the military, and nearly every domain you can imagine. Each has terms and concepts that mean something very particular to them. A secret language enables complex topics to be covered in concise and meaningful dialogue without the need for confusing babble. It’s vital for a development team to understand and collaborate on this language, known as the ubiquitous language (UL). The UL’s terms and concepts are used when communicating with team members, including domain experts. They’re also used to name classes, methods, and namespaces in the codebase.
商业语言是一种丰富的方言,其术语描述性强且富有洞察力。然而,如果开发团队没有与领域专家合作,无法充分理解该语言并将其应用于代码实现,那么它的大部分优势就会丧失。开发人员反而会针对问题领域创建自己的语言和抽象概念集。如果没有共享的模型和实践语言(UL),开发团队与领域专家之间的有效沟通将面临挑战,需要某种形式的翻译。从领域概念到技术概念的翻译可能非常耗时且容易出错。当代码实现团队使用的模型与领域专家的模型不同时,重要的领域洞察力可能会丢失。此外,团队需要进行冗长而复杂的沟通来解释软件实现中遇到的问题,而这些问题本可以通过更好地理解问题领域并采用更有效的沟通方式轻松解决。
The business language is a rich dialect with highly descriptive and insightful terminology. However, if the development team doesn’t engage with domain experts to fully understand the language and use it within the code implementation, much of its benefit is lost. Developers instead create their own language and set of abstractions for a problem domain. Without a shared model and UL, effective communication between the development team and domain experts is a challenge and requires some form of translation. Translation from domain concepts to technical concepts can be time consuming and error prone. Vital domain insights can be lost when the team implementing the code is using a different model than that of the domain expert. Furthermore, lengthy and convoluted communication is required to explain problems that the team faces in a software implementation that could be solved easily with a better understanding of the problem domain and a more efficient way of communicating.
图 4.7展示了开发人员思维中的不同模型如何导致与领域专家的沟通出现问题。在代码中,开发人员专注于技术抽象、设计模式和设计原则,而领域专家则专注于业务流程和工作流。
Figure 4.7 shows how a different model in the minds of a developer can make communication with the domain expert problematic. In the code, the developer is focused on technical abstractions, design patterns, and design principles, whereas the domain expert is focused on business process and work flow.
图 4.7项目的翻译成本。
FIGURE 4.7 Translation costs of the project.
开发人员应该用领域术语和概念来思考,而不是技术术语,以避免将业务术语翻译成技术术语。如果开发团队在翻译复杂的逻辑和工作流程时犯了错误,那么代码中出现 bug 的可能性就会大大增加。
Developers should think in domain terms and concepts, not technical terms, to avoid the need to translate from business jargon into technical jargon. If the development team makes a mistake when translating complex logic and work flow, the chance of creating a bug in code significantly increases.
企业用来描述其业务的丰富语言是 UL 的要素之一。然而,在创建问题域模型并将其转化为代码实现时,您可能需要创建新的概念和术语。企业使用的术语可能与 IT 社区的术语非常相似,有些术语过于通用。开发团队和领域专家需要创建新术语并明确定义现有术语的含义,以便在代码中实现该模型。
The rich language that the business uses to describe what it does is one ingredient of the UL. However, when creating a model of the problem domain and implementing it in code, you may need to create new concepts and terminology. The business may use jargon much in the same way that the IT community does, with some terms proving to be too generic. The development team and domain experts need to create new terms and explicitly define the meaning of existing terms to implement the model in code.
当团队在代码中实现模型时,可能会出现新的概念,这些概念通常会被一系列需要命名的逻辑所强调。这些发现的术语需要反馈给领域专家进行验证和澄清。
As teams are implementing the model in code, new concepts may appear, often highlighted by a collection on logic that needs to be named. These discovered terms need to be fed back to the domain experts for validation and clarification.
开发团队不仅必须学习业务中明确的术语和概念,还必须与领域专家合作,定义那些可能没有术语的假设或隐含概念。这些概念必须由整个团队命名,并包含在共享的UL中。团队可能还需要为问题领域中不存在但在软件建模过程中发现并标记的概念创建术语。
Not only must the development team learn the explicit terms and concepts from the business, but they must collaborate with the domain experts to define the assumed or implicit concepts that may not have terminology. These concepts must be named by the entire team and included in the shared UL. The team may also need to create terms for concepts that don’t exist in the problem domain but have been discovered and labeled during modeling in the software.
团队成员必须使用 UL 相互沟通。开发团队必须在代码中使用它,领域专家在与团队沟通时也必须使用它。使用共享语言,无需将业务语言翻译成技术语言,反之亦然。由于每个人都理解概念背后的含义,它还能消除歧义和误解的可能性。
The team members must communicate with each other using the UL. The development team must use it in code, and the domain experts must use it when talking to the team. A shared language removes the need to translate from business speak into technical language and vice versa. It also removes the possibility of ambiguity and misinterpretation because everyone understands the meaning behind the concepts.
UL 应该清晰简洁。应删除技术术语,以免分散对业务概念的注意力。同样,与正在开发的软件无关的领域术语也不得混淆通用语言。
The UL should be clear and concise. Technical terms should be removed so they don’t distract from business concepts. Likewise, domain terms not relevant for the software under creation must not be allowed to cloud the shared language.
正如第 2 章“提炼问题域”中提到的,为了更好地理解你所在的领域,最好采取领域行为的具体示例。真实场景的具体示例有助于巩固领域内的流程和概念。然而,重要的是揭示业务流程的意图,而不是具体实现。只用业务术语进行讨论,不要涉及技术细节。
As mentioned in Chapter 2, “Distilling the Problem Domain,” to better understand the domain you’re in, it’s a good idea to take specific examples of domain behavior. Concrete examples of real scenarios help to cement processes and concepts within the domain. However, it’s important to reveal the intention of the business process and not the implementation. Talk only in business terms; don’t get technical.
在以下对话中,一位商业用户正在描述电子商务网站上的客户请求更换未送达的订单的过程:
In the following dialogue, a business user is describing the process of customers at an e-commerce site requesting a replacement for an order that wasn’t delivered:
如果顾客未收到商品,可以免费申请新的订单。她登录账户,点击“我未收到商品”按钮。如果她已被标记为已收到免费订单,则必须联系客服才能申请新的订单。否则,我们将向她发送免费订单,并更新数据库,显示该顾客已申请丢失商品。之后,我们会联系快递员,看看能否索回丢失订单的费用。
When a customer doesn’t receive her goods, she can request a new order for free. She logs into her account and clicks on the I Have Not Received My Items button. If she has already been flagged as having received a free order, she can’t get another one without speaking to customer service. Otherwise, we will send her a free order and update the database to show that this customer has already claimed for a lost item. We will then contact the courier to see if we can claim back the cost of the lost order.
您会在描述中注意到,业务用户关注的不是业务流程,而是实现问题。以下句子对领域或业务流程没有任何价值或见解:
You will notice in the description that the business user is not focusing on the business process, but rather the implementation concerns. The following sentence gives no value or insight into the domain or business process:
她登录自己的帐户并点击“我尚未收到我的物品”按钮。
She logs into her account and clicks on the I Have Not Received My Items button.
下一句中,业务用户已经开始质疑你该如何实施业务策略了。有些专家可能有数据库经验,甚至可能会建议数据模式。同样,这说明团队对该领域缺乏深入的理解:
In the next sentence, the business user is already second-guessing how you will implement the business policy. Some experts may have experience with databases and may go as far as suggesting data schemas. Again, this gives the team no deep understanding of the domain:
如果她已经被标记为收到免费订单,那么她就不能再获得另一个订单,除非与客服联系。
If she has already been flagged as having received a free order, she can’t get another one without speaking to customer service.
从这组需求来看,对领域不感兴趣的团队可能会简单地按照要求执行,最终得到一个糟糕的模型,无法反映领域的概念和策略。这可能会导致误解“标记客户”的含义;它可能不仅仅是在数据库列中打勾,甚至可能是启动单独业务工作流的催化剂。如果不了解领域和功能意图,开发人员就不会意识到仅仅按照要求执行会带来什么后果。
From this set of requirements, a team not interested in the domain may simply implement what it is told and end up with a poor model that doesn’t reflect the concepts and policies of the domain. The impact of this could be a misunderstanding of what “flagging the customer” means; it may mean more than simply a tick in a database column and perhaps the catalyst for the start of a separate business work flow. Without understanding the domain and the intent of a feature, the developers won’t appreciate the repercussions of just implementing what they are told.
培训和协作将帮助业务人员专注于流程而非实施,专注于问题空间而非解决方案空间。接下来,之前的需求陈述已使用领域语言重写。它侧重于业务及其流程:
Training and collaboration will help business people focus on the process rather than the implementation and the problem space rather than the solution space. Next, the previous requirements statement has been rewritten using the language of the domain. It focuses on the business and its processes:
如果您尚未收到订单,可以提交未送达订单通知。如果这是您的首次索赔,则会创建替换订单。如果您之前已提出索赔,则会开立索赔案件并分配给客户服务代表,该代表将调查此索赔。在所有情况下,都会开立丢失邮件赔偿案件,并向快递员发送未送达货件的详细信息。
If you have not received an order, you can submit an undelivered order notification. If this is your first claim, a replacement order is created. If you have made a claim before, your claim case is opened and assigned to a customer service representative, who will investigate the claim. In all cases, a lost mail compensation case is opened and sent to the courier with details of the consignment that was undelivered.
在此描述中,您发现了许多之前缺失的重要领域概念。重写的文字将一些术语引入了UL,并且该领域的术语已经已经非常清晰了。事实上,第二个描述甚至不包含客户概念;相反,它只关注与流程直接相关的术语。
In this description, you have discovered many important domain concepts that were missing before. The rewritten prose introduces some terms into the UL, and the terminology of the domain has been made crystal clear. In fact, the second description doesn’t even contain the customer concept; instead, it focuses only on terms that are directly related to the process.
请记住:领域专家对技术术语缺乏理解,或者理解有限。示例应以业务为中心,如果领域专家试图通过跳到实现细节来帮助你,只需温和地提醒他们关注系统是什么以及为什么,并让他们把如何实现留给你。
Remember: domain experts have no, or limited, understanding of technical terminology. Keep examples focused on the business, and if domain experts are trying to help you by jumping to implementation details, just gently remind them to focus on the what and the why of a system and ask them to leave the how up to you.
以下最佳实践可帮助您塑造 UL。
The following best practices can help to shape your UL.
丰富的领域模型是为了解决复杂的问题而构建的,创建有效领域模型的最佳方法是首先关注应用程序中对业务至关重要的部分。忽略系统中那些仅管理数据以及大多数操作基于 CRUD 的部分。相反,要寻找最难的部分,即业务最关心的核心领域,以及通常对盈利或省钱至关重要的部分。
Rich domain models are built to satisfy complex problems, the best way to create effective domain models is to firstly focus on areas of the application that are important to the business. Ignore the parts of a system that simply manage data and where most of the operations are CRUD based. Instead look for the hard parts, the areas in the core domain that the business cares passionately about and often the parts that are key to making or saving money.
一个常见的误解是,领域模型应该贴合现实;事实上,你根本不应该试图模拟现实生活,而应该模拟问题领域内有用的抽象。寻找问题领域内的共性和差异。了解哪些可能会发生变化,哪些被认为是复杂的。利用这些信息来构建你的模型。这比基于问题领域的世界来识别名词和动词更有用。最重要的是,只建模满足业务场景需求的内容。
A common misunderstanding is that a domain model should match reality; in fact, you should not look to model real life at all but rather model useful abstractions within the problem domain. Look for commonalities and variations within the problem domain. Understand which are likely to change and are considered complex. Use this information to build your model. It will be far more useful than identifying nouns and verbs based on the world of the problem domain. Most importantly model only what is needed to meet the need of the business case scenario.
领域模型并非现实生活的模型;它是对现实的抽象系统,是一种仅包含问题领域中那些在解决特定业务用例时普遍存在的方面的解释。领域模型应该排除任何与解决问题无关的领域细节。图 4.8所示的伦敦地铁图旨在解决问题。它并不反映现实生活。它对于计算伦敦地标之间的距离没有用处,但对于乘坐地铁出行却很有用。在它被设计的语境下,它简单有效。
A domain model is not a model of real life; it is a system of abstractions on reality, an interpretation that only includes aspects of the problem domain that are prevalent to solving specific business use cases. A domain model should exclude any irrelevant details of a domain that do not serve to solve problems. The London Tube map shown in Figure 4.8 was designed to solve a problem. It doesn’t reflect real life. It isn’t useful for calculating distances between landmarks in London, but it is useful for traveling on the underground. It’s simple and effective within the context that it was designed for.
图 4.8伦敦地铁地图与车站之间的距离几乎没有相似之处。
FIGURE 4.8 London Tube map bearing little resemblance to the distance between stations.
由于领域模型与实际建模无关,因此不能将其视为对错。相反,应该将其视为对其所要解决的特定问题是否有用。
Because it’s not concerned with modeling real life, the domain model cannot be deemed as being wrong or right. Rather, it should be viewed as useful or not for the given problem it is being used to solve.
创建有效的领域模型是 DDD 的基础。它是知识梳理与共享、设计洞察和突破的产物。拥有一个功能强大且富含 UL 的模型是实现问题领域业务目标的关键。创建有用的领域模型很难,需要大量的探索、实验、协作和学习。
Creating an effective domain model is fundamental to DDD. It is the artifact of knowledge crunching and sharing, design insight, and breakthroughs. Having a useful model that is rich in the UL is the key to meeting business objectives in the problem domain. Creating a useful domain model is hard and takes lots of exploration, experimentation, collaboration, and learning.
领域模型存在的原因只有一个:服务于正在开发的应用程序。记住,创建领域模型时要有所选择;不必包含所有内容。企业规模庞大、复杂,涉及方方面面。试图在单一模型中构建这样的世界,往好了说是愚蠢的,往坏了说是极其耗时且毫无意义的。毋庸置疑,这将是一场维护的噩梦。如果您正在建模一个大型系统,请通过清晰地划分模型的各个部分,将其分解成更易于管理的块。
The domain model exists for one reason: to serve the application under development. Remember to be selective when creating your domain models; you don’t have to include everything. Businesses are big and complex with a lot going on. Trying to create that world within a single model would be at best foolish and at worst extremely time consuming and rather pointless. Needless to say, it would be a maintenance nightmare. If you are modeling a large system, break it down to more manageable chunks by clearly sectioning off parts of the model.
尽量不要对真实的关系进行建模;相反,应该根据系统中的不变量和规则来定义关联(有意义的连接)。在现实生活中,客户既有信用记录,也有联系电子邮件地址,但你有多少次会遇到这样的规则:要求你拥有良好的信用记录和以“A”开头的电子邮件地址才能购买商品?相反,应该将行为和数据分组,以满足问题领域的需求,而不是你认为可能属于同一类的情况。请记住,你创建的模型是为了满足一个业务用例(或一组业务用例)的需求,而不是试图模拟现实生活。
Try not to model real relationships; instead, define associations (meaningful connections) in terms of invariants and rules in the system. In real life, a customer has both a credit history and a contact e-mail address, but how often would you come across a rule requiring you to have a good credit history and an e-mail address starting with “A” to be able to purchase an item? Instead, group behavior and data to satisfy the needs of the problem domain rather than what you think might belong together. Remember that you are producing a model to fulfill the needs of a business use case (or set of business use cases), not trying to model real life.
为了保持领域模型的相关性和专注性,你应该不断根据新场景挑战你创建的模型,并与领域专家验证你的理解。移除任何不再相关的行为,以避免噪音。
To keep your domain model relevant and focused, you should constantly challenge the model you create against new scenarios and validate your understanding with domain experts. Remove any behavior that is no longer relevant to avoid noise.
领域模型需要不断完善才能持续发挥作用。领域模型只能在给定的迭代和用例集中暂时发挥作用。未来的用例或业务变更可能会导致该模型失效。领域模型代表了共享语言的实现,但仅适用于当前时刻。基于这种理解,开发人员不应过分执着于优雅的模型。如果模型变得不再适用,他们需要愿意将其抛弃并重新开始。
A domain model needs to be constantly refined to continually be useful. A domain model is only ever temporarily useful for a given iteration and set of use cases. Future use cases or changes to the business may render the model useless. The domain model represents an implementation of the shared language that is applicable for only that moment in time. It is with this understanding that developers should not be too attached to an elegant model. They need to be willing to rip up and start again if the model becomes irrelevant.
有效沟通是解决问题的最重要技能。开发人员的目的不是编写代码,而是解决问题。正因如此,使用一种没有歧义或无需翻译的语言与你的业务方沟通至关重要。通过消除语言障碍,领域专家和开发团队可以自由协作、探索和试验实用模型的设计。这样,技术实现就可以使用相同的通用语言 (UL) 来表达,任何设计见解都可以反馈给领域专家进行验证,而无需翻译,也不会造成语义损失。
Being able to communicate effectively is the most important skill for solving problems. A developer’s purpose is not to code; it’s to solve problems. That’s why it’s vital to talk to the business you are working for in a language without ambiguity or need of translation. By removing linguistic barriers, domain experts and the development team are free to collaborate, explore, and experiment with designs for a useful model. Technical implementations can then be expressed using the same UL, and any design insights can then be fed back to domain experts for validation without need for translation and loss of meaning.
仅为了实现共通性而引入抽象,即便如此,也尽量避免使用它们。抽象是有代价的。与其担心重复,不如明确表达,因为试图将松散相关的概念绑定到超类下可能会导致代码维护问题。
Introduce abstractions for commonality only, and even then try and avoid them. Abstractions come at a cost. It is far better to be explicit than worry about not repeating yourself as trying to tie loosely related concepts under a super class can cause problems with code maintenance.
抽象类或接口应该代表领域内的一个想法或概念。限制代码库中的抽象非常重要,并且只为领域内有变体的概念创建抽象。不要试图抽象所有领域关注点。如果它不是某个概念的变体,那么就保持其具体性,并且只有在创建了它的变体时才抽象。记住,与其将重要的领域概念隐藏在层层不必要的抽象之后,不如将其明确表达。
An abstract class or an interface should represent an idea or a concept in your domain. It is really important to limit abstractions in your code base and only create them for concepts in your domain that have variations. Don’t seek to abstract every domain concern. If it’s not a variation of a concept then keep it concrete and only abstract if, and when, you create a variation of it. Remember it is always better to be explicit rather than hiding an important domain concept behind layers of needless abstraction.
那么什么时候应该抽象呢?以上班为例。抽象概念是通勤,而步行、坐火车或开车是该概念的变体;也就是具体实现。如果上班没有变体(即我们都开车),我们就不需要引入通勤这样的抽象概念。
So when should you abstract? Take the example of traveling to work. The abstract concept would be to commute whereas walking, taking the train, or driving is a variation of that concept; i.e., the concrete implementation. If there were no variation in traveling to work (i.e., we all drove) we would not need to introduce an abstract concept such as commuting.
有效的领域模型应该通过将代码置于适当的抽象层次来表达业务用例的意图。读者应该能够快速掌握领域概念,而无需深入代码来理解实现细节。
An effective domain model should express the intent of the business use case by aiming code at the right level of abstraction. Readers should be able to quickly grasp domain concepts without having to drill down into the code to understand the implementation details.
在高层次上创建抽象;当您需要重构模型以处理新场景或实现设计突破时,低层次上的过多抽象将造成很大的摩擦。
Create abstractions at a high level; too many abstractions at a low level will cause a great amount of friction when you need to refactor your model to handle a new scenario or when you have a design breakthrough.
引入抽象总是有代价的,所以我们必须谨慎地将其应用到正确的层次,以及能够从中受益的代码区域。在底层,我们应该避免抽象,而应该倾向于将行为组合到显式的具体对象中。抽象会在类之间产生依赖关系,而依赖越多,代码耦合度就越高。
There is always a cost to introducing abstraction so we must be careful to apply it at the right level and to areas of code that will benefit from it. At a low level we should avoid abstraction and instead favor composition of behavior from explicit concrete objects. Abstraction creates a dependency between classes and more dependencies equate to higher code coupling.
您不应该针对特定问题创建抽象;抽象代表一般概念,例如订单处理应用程序的 IShippingNoteGenerator。由于所需纸质工作存在差异,此概念的变体可能在国内或国际上存在。不要自动抽象相关概念。继续讨论履行领域,不要尝试为快递网关创建通用抽象;它们不代表领域行为,而是基础设施问题。相反,应使这些实现具体、明确,并且独立于领域模型。当我们谈论领域概念时,我们实际上谈论的是领域行为。根据行为创建抽象类或接口;保持它们的简洁和专注。问问自己,领域行为有多少种变化?不要强制抽象;仅当它有助于在模型中更清晰地表达概念时才使用它。
You shouldn’t have an abstraction that is specific for a particular problem; abstractions represent general concepts such as a IShippingNoteGenerator for an order processing application. Variations of this concept could be domestic and international due to the differences between paper work required. Don’t automatically abstract concepts that are related. Continuing with the fulfillment domain, don’t try and create a common abstraction for courier gateways; they don’t represent domain behavior, they are infrastructural concerns. Instead keep these implementations concrete, explicit, and out of the domain model. When we talk about domain concepts we are really talking about domain behavior. Create abstract classes or interfaces based on behavior; keep them small and focused. Ask yourself how much variation is there in domain behavior? Don’t force abstraction; use it only when it will help to express concepts clearer in you model.
正如设计模式在重构代码时会浮现,领域概念也会随之出现。当它们出现并且你发现了概念的变体时,就可以以接口或抽象类的形式引入抽象。同时要注意不要过早重构。如果你对领域不够了解,那么你可能不知道重构的最佳方法。与其让自己陷入困境,不如让代码在几次迭代中逐渐增长,然后观察相关行为周围出现的自然模式。有了这种清晰的思路,你就能更好地开始重构和引入抽象。
Just as design patterns emerge when you refactor code so will domain concepts. When they do and you find variations of the concept then you can introduce abstractions in the form of interfaces or abstract classes. Also be mindful of premature refactoring. If you don’t know the domain well enough then you may not know the best way to refactor. Instead of painting yourself into a corner let the code grow for a few iterations then look to see natural patterns appear around related behavior. With this clarity you will be in a much better place to start to refactor and introduce abstractions.
审视系统中的所有抽象。你的接口和基类能告诉你关于应用程序领域的哪些信息?它们应该揭示系统中的主要概念,而不仅仅是每个实现的抽象。
Look at all of the abstractions in your system. What do your interfaces and base classes tell you about the domain of your application? They should reveal the major concepts in your system and not just be abstractions of each implementation.
至关重要的是,要根据领域场景,用代码测试你的设计,以确保你的白板思维能够发挥作用,并发现任何需要修改或妥协模型的技术限制。技术实现将揭示设计中的任何问题,并有助于巩固你对问题领域的理解。
It is vitally important to test your design in code against domain scenarios to ensure your white board thinking can work as well as discovering any technical constraints that require an alteration or compromises to the model. Technical implementations will reveal any problems with the design and will help cement your understanding of a problem domain.
只有当你的想法耗尽时才停止建模,而不是在你有了第一个好主意时就停止。一旦你有了一个有用的模型,就重新开始。挑战自己以不同的方式创建模型,试验你的思维和设计技巧。尝试用一个完全不同的模型来解决问题。如果第一次没有成功,就重构出一个更好的解决方案。不断重构你对问题领域的理解,以生成更清晰的模型。模型会随着知识的积累而改变。
Only stop modelling when you have run out of ideas and not when you get the first good idea. Once you have a useful model start again. Challenge yourself to create a model in a different way, experiment with your thinking and design skills. Try to solve the problem with a completely different model. If you don’t get it right the first time, refactor to a better solution. Constantly refactor to your understanding of the problem domain to produce a more express model. Models will change with more knowledge.
记住,模型只是暂时有用的;不要执着于华丽的设计。抛弃模型中不再有用的部分,并准备好在新的用例和场景出现时进行修改。
Remember a model is only useful for a moment in time; don’t get attached to elegant designs. Rip up parts of your model that are no longer useful, and be willing to change when new use cases and scenarios are thrown at your design.
简单的问题不需要复杂的解决方案。您无需为整个应用程序创建 UL。与领域专家一起,将精力集中在复杂或重要的核心领域上。对于通用/支持领域,不要浪费您的精力,尤其是在没有领域逻辑的情况下;这样做会让您忙碌的领域专家感到沮丧,并使他们不愿意帮助您解决应用程序中的复杂问题。
Simple problems don’t require complex solutions. You don’t need to create a UL for your entire application. Focus your efforts with domain experts on the complex or important core domain. For generic/supporting domains don’t waste your efforts, epically if there is no domain logic; doing so will frustrate your busy domain experts and leave them reluctant to help out with the complex areas of your application.
当您遇到复杂的领域,您在与利益相关者沟通时遇到困难,或者您的团队在您没有太多经验的领域中工作时,这是突破、建模和研究 UL 的时候了。
When you come across an area of complexity, you’re having trouble communicating with the stakeholder, or your team is working in part of the domain that you don’t have much experience with, this is the time to break out, model, and work on the UL.
始终挑战自己并提出以下问题:“我的工作是否在核心子域内进行?这个问题是否需要丰富的领域?业务部门是否关心应用程序的这个领域?它会带来什么改变?它对业务重要吗?他们对此抱有很高的期望吗?还是他们只是希望它能够发挥作用?”
Always challenge yourself and ask the questions, “Am I working within the core subdomain? Does this problem require a rich domain? Does the business care about this area of the application? Will it make a difference? Is it important to the business and do they have high expectations of it or do they just want it to work?”
如果你遇到一个特别棘手且复杂的边缘情况,并且它位于系统非核心区域,那么你应该考虑将其变为手动流程。与其处理边缘情况,不如将其明确地变为手动流程,这样可以节省宝贵的时间,让你能够更好地资源用于核心领域的工作。人类和手动流程擅长处理边缘情况,并且通常可以根据需要大量时间复制的数据做出决策。
If you have a particularly nasty and complex edge case that is in an area of the system that is not core then you should think about making it a manual process. Not handling edge cases and making them an explicit manual process instead can save valuable time and give you more resources to work on the core domain. Humans and manual processes are great at edge cases and can often make decisions based on data that would take a considerable amount of time to replicate.
应用程序的核心领域决定了它被构建而非购买的原因。它是利益相关者最热衷的领域,也是你们可以进行有趣的对话和宝贵的知识交流的地方。这是你的UL能为你带来最大价值,也需要你专注的领域。尽量不要为整个领域创建丰富的语言,因为许多支持领域和通用领域不需要,而且只会浪费精力。将精力集中在能带来价值的领域上。尽量不要为所有领域都创建UL。不复杂的领域和子领域不会从UL中受益,所以不要分散精力。核心领域很小;专注于它。创建UL的成本很高。
The core domain of your application is why it is being built rather than bought. It is what your stakeholders are most passionate about, and where you can have interesting conversations and valuable knowledge-crunching sessions. This is the area where your UL gives you the most value, and demands your focus. Try not to create a rich language for your entire domain because many of your supporting and generic domains do not require one and are a waste of effort. Focus your efforts on what gives you value. Try not to create a UL for everything. Areas and subdomains that are not complex will not benefit from a UL, so don’t spread yourself too thin. A core domain is small; focus on it. Creating a UL is costly.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第五章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 5 download and individually named according to the names throughout the chapter.
DDD 的重点是管理复杂性。正如您所读到的,这是通过将领域模型置于软件的中心来实现的,该模型可以实现应用程序的行为。您可以使用各种模式以代码形式表示该模型。在第三章“聚焦核心领域”中,我们介绍了子领域以及大型应用程序中可能存在多个模型的现实。然而,并非所有模型都具有相同的复杂性或重要性。有些模型包含复杂的领域逻辑,而另一些模型仅负责数据管理,因此,选择最合适的设计模式以代码形式表示模型是明智之举。
The focus of DDD is to manage complexity. As you have read, this is achieved by placing a model of the domain at the center of your software to fulfill the behaviors of your application. There are various patterns at your disposal to represent the model in code form. In Chapter 3, “Focusing on the Core Domain,” you were introduced to subdomains and the reality that more than one model may exist in large applications. However, not all models will be of equal complexity or importance. Some will contain complex domain logic, while others will simply be responsible for the management of data, therefore it is wise to choose the most appropriate design pattern to represent the model in code.
重要的是要理解,在选择表示领域逻辑的模式时,没有最佳实践。只要将领域逻辑与技术问题隔离开来,就可以实现模型驱动设计,从而实现领域驱动设计。
It is important to understand that there is no best practice when it comes to selecting a pattern to represent your domain logic. As long as you isolate domain logic from technical concerns you can implement Model-Driven Design and hence Domain-Driven Design.
本章介绍实现领域模型时可用的设计模式。除了解释每种模式之外,还会给出何时最适合使用该模式以及何时最好避免使用该模式的建议。
This chapter presents the design patterns at your disposal when implementing a domain model. Along with an explanation of each pattern, advice will be given on when it’s most appropriate to use the pattern and when it is best to be avoided.
领域层是应用程序的核心,是包含领域模型的代码区域。它将领域模型的复杂性与应用程序的偶然技术复杂性隔离开来。它负责确保基础设施问题(例如管理事务和持久化状态)不会渗透到业务问题中,并模糊领域中现有的规则。在大多数情况下,领域层仅占应用程序的一小部分。其余部分由基础设施和展示职责组成,如图5.1所示。
The domain layer, at the heart of your application, is the area of code that contains your domain model. It isolates the complexities of the domain model from the accidental technical complexities of the application. It is responsible for ensuring that infrastructure concerns, such as managing transactions and persisting state, don’t bleed into the business concerns and blur the rules that exist in the domain. In most cases, the domain layer makes up only a small part of your application. The rest is filled with infrastructure and presentation responsibilities, as you can see in Figure 5.1.
图 5.1表示领域模型的代码仅占整个代码库的一小部分。
FIGURE 5.1 The code that represents the domain model makes up only a small portion of the overall codebase.
您可以使用多种模式在代码中实现领域模型。大型系统的构建方式各不相同。有些部分的重要性不如其他部分,并且存在多个模型来服务于不同的场景。图 5.2展示了一个应用程序中共存的多个模型。这是因为不同的场景或不同的团队负责不同的工作,需要不同的模型。模型。第六章“使用有界上下文维护领域模型的完整性”将更详细地探讨模型的边界。现在,请理解可以有多个模型在起作用,并且您可以用不同的方式实现这些模型。图 5.2展示了一个示例,说明如何将大型应用程序分割成不同的上下文,并使用不同的模式来表示领域模型。本章的其余部分将探讨在对领域进行建模时可以遵循的设计模式。以下三种模式最初是在 Martin Fowler 所著的《企业应用程序架构模式》一书中提出的。
There are various patterns at your disposal to implement a domain model in code. Large systems are not all built in the same way. Some parts are less important than others, and multiple models exist to serve different contexts. Figure 5.2 shows multiple models coexisting in an application. This is because different models are required for different contexts or different teams working on separate models. The boundaries around the model are explored in more detail in Chapter 6, “Maintaining the Integrity of Domain Models with Bounded Contexts.” For now, understand that multiple models can be at play, and you can implement those models in different manners. Figure 5.2 shows an example of how a large application can be segmented into contexts with different patterns used to represent the domain model. The rest of this chapter explores the design patterns that you can follow when modeling a domain. The following three were first presented in the book Patterns of Enterprise Application Architecture, by Martin Fowler.
图 5.2应用程序内部以各种模式实现的多个领域模型。
FIGURE 5.2 Multiple domain models implemented in various patterns inside an application.
除了 Fowler 的模式之外,本章还将介绍活动记录模式、贫血领域模型模式,以及用于在代码中实现模型的函数式模式。本章介绍的每种模式都很有用,具体取决于应用程序中每个模型的复杂程度。
In addition to Fowler’s patterns, you will be introduced to the active record pattern, the anemic domain model pattern, as well as functional patterns for implementing a model in code. Each of the patterns presented in this chapter are useful depending on the complexities of each model in your application.
Martin Fowler 在其《企业应用架构模式》一书中介绍了领域模型模式,该模式与 DDD 同义,因为它非常适合处理具有丰富业务逻辑的复杂领域。领域模型是一个面向对象的模型,它融合了行为和数据。乍一看,它可能与数据持久化模型(如果您使用的是关系数据库,则为数据模式)相似。虽然两者都包含数据,但领域模型还融合了业务流程和关联、规则以及丰富的领域逻辑。DDD 提供了许多构建块模式(将在第三部分中介绍),这些模式将帮助您更有效地实现 Fowler 的领域模型模式。
The domain model pattern, catalogued in Martin Fowler’s Patterns of Enterprise Application Architecture, is synonymous with DDD because it is a good fit for complex domains with rich business logic. The domain model is an object-oriented model that incorporates both behavior and data. At first glance, it may mirror the data persistence model (data schema if you are using a relational database). Although both contain data, the domain model also incorporates business process and associations, rules, and rich domain logic. DDD offers a number of building block patterns, which are covered in Part III, which will enable you to implement Fowler’s domain model pattern more effectively.
领域模型模式基于无数据库的前提;因此,它可以以完全忽略持久性的方式演进和创建。在设计模型时,您无需从数据模型入手,而是从代码模型入手——这是模型驱动的设计,而非数据驱动的设计。只有当您必须考虑模型的持久化时,才可以在设计上做出妥协。模型中的领域对象被称为普通旧式 C# 对象 (POCO)。这些类无需考虑基础设施,并且完全忽略持久性。图 5.3展示了领域模型模式与技术基础设施是如何分离的。
The domain model pattern is based on the premise that there is no database; therefore, it can evolve and be created in a completely persistence-ignorant manner. When designing the model, you don’t start with a data model; instead, you start with the code model—model-driven as opposed to data-driven design. Only when you have to think about persisting the model can you compromise on the design. Domain objects within the model are known as Plain Old C# Objects (POCO). These classes are free from infrastructure concerns and are completely persistence ignorant. Figure 5.3 shows how the domain model pattern and technical infrastructure are kept separated.
图 5.3领域模型模式。
FIGURE 5.3 The domain model pattern.
这种只关注领域模型的能力使得领域逻辑的设计能够由领域抽象驱动——这就是 DDD。通过不考虑持久化需求,您可以构建一个纯粹专注于当前领域问题的富有表现力的模型。当然,您需要对其进行持久化,并且可能需要做出妥协,但在建模时您不应该考虑这些。这使得领域模型摆脱了基础架构代码的束缚,而只专注于领域逻辑。
This ability to focus only on the domain model enables the design of the domain logic to be driven by the abstractions of the domain—hence, DDD. By not thinking about persistence needs, you can build an expressive model purely focused on the domain problem at hand. Of course you will need to persist it and may need to compromise, but you should not think about this when modelling. This keeps the domain model free of infrastructural code and focused only on domain logic.
您可以将领域模型视为一个概念层,它代表您正在处理的领域。事物存在于此模型中,并与其他事物之间存在关联。例如,如果您正在构建一个电子商务商店,那么存在于模型中的“事物”将代表购物篮、订单、订单项等等。这些事物不仅拥有数据,更重要的是,它们拥有行为。订单不仅拥有代表创建日期、状态和订单号的属性,还包含应用折扣券的业务逻辑,以及围绕它的所有领域规则:优惠券是否有效?优惠券可以与购物篮中的产品一起使用吗?是否存在其他会导致优惠券无效的优惠?
You can think of a domain model as a conceptual layer that represents the domain you are working in. Things exist in this model and have relationships to other things. For example, if you were building an e-commerce store, the “things” that would live in the model would represent a Basket, Order, Order Item, and the like. These things have data and, more importantly, they have behavior. Not only would an order have properties that represent a creation date, status, and order number, but it would contain the business logic to apply a discount coupon, including all the domain rules that surround it: Is the coupon valid? Can the coupon be used with the products in the basket? Are there any other offers in place that would render the coupon invalid?
图 5.4展示了一个在线拍卖网站的部分领域模型。模型中的对象代表了问题领域的概念,用于实现拍卖的行为。正如你所见,该模型与拍卖领域的名词相对应,但情况并非总是如此。事实上,在建模时,你应该关注问题领域的动词和动作,因为这将帮助你专注于行为而不是状态,最终帮助你创建面向对象的数据模型表示。
Figure 5.4 shows part of the domain model for an online auction site. The objects in the model represent concepts of the problem domain that are used to fulfill the behaviors of an auction. As you can see, the model aligns with the nouns of the auction domain but this is not always the case. In fact you should focus on the verbs and actions of a problem domain when modeling as this will help you concentrate on behavior rather than state, which could end up with your creating an object-oriented representation of the data model.
图 5.4拍卖网站的领域模型。
FIGURE 5.4 The domain model of an auction site.
在领域模型中,每个对象负责一项特定的任务。对象之间通过相互委托来协同工作,完成业务用例。在清单 5-1 中,您可以看到 Auction 类如何委托 WinningBid 来确定下一个出价增量。
In a domain model each object is responsible for a specific task. Objects work together to fulfill business use cases by delegating to each other. In Listing 5-1 you can see how the Auction class delegates to a WinningBid to determine the next price of the bid increment.
当您需要建模一个复杂、丰富且复杂的业务领域时,领域模型会非常有效。它是一种纯粹的面向对象方法,涉及创建真实业务领域的抽象模型,在处理复杂的逻辑和工作流时非常有用。领域模型不依赖持久层,依赖于映射器类和其他抽象模式来持久化和检索业务实体。如果您需要建模复杂的逻辑或问题域中某个部分,而该部分非常重要,需要清晰的表达,或者由于持续投入而经常发生变化,那么领域模型模式是一个不错的选择。
The domain model excels when you have an involved, rich, complex business domain to model. It’s a pure object-oriented approach that involves creating an abstract model of the real business domain and is useful when dealing with complex logic and workflow. The domain model is persistence ignorant and relies on mapper classes and other abstraction patterns to persist and retrieve business entities. If you have to model complex logic or part of the problem domain that requires clarity because it’s important, or will change often due to continued investment, it’s a good candidate for the domain model pattern.
领域模型模式并非灵丹妙药,因为实现成本可能很高。它在技术上最具挑战性,需要开发人员精通面向对象编程。大多数子系统都是基于 CRUD 的,只有核心领域才需要领域模型实现模式来确保清晰度或管理复杂的逻辑。你不应该尝试将领域模型模式应用于所有内容。应用程序的某些部分可能只是基于数据的表单,只需要基本验证,而无需丰富的业务逻辑。试图对所有事物进行建模并应用面向对象实践只会浪费精力,这些精力最好花在你的核心领域上。软件开发就是为了让事情变得更简单,所以如果你有复杂的逻辑,请应用领域模型模式;否则,寻找适合你所遇到问题的模式,例如贫血领域模型或表模块模式。
The domain model pattern is no silver bullet as it can be costly to implement. It’s the most technically challenging and requires developers with a good grasp of object-oriented programming. The majority of sub systems are CRUD based, with only the core domain requiring the domain model implementation pattern to ensure clarity or to manage complex logic. What you should not do is try to apply the domain model pattern for everything. Some parts of your application will simply be forms over data and will require just basic validation instead of rich business logic. Trying to model everything and apply object-oriented practices would be a waste of effort that would be better spent on your core domain. Software development is all about making things simpler, so if you have complex logic, apply the domain model pattern; otherwise, look for a pattern that fits the problem you have, like the anemic domain model or the table module pattern.
如果你正在开发的应用程序部分没有频繁变化的逻辑,而只是一种数据形式,那么最好不要尝试应用领域模型模式。最多也就是错误地使用领域模型模式可能会导致精力的浪费;最糟糕的是,你会引入不必要的复杂性,而使用更简单的实现方法就足够了。
If the portion of the application you are working on does not have frequently changing logic and is merely a form of data, it is best not to try to apply the domain model pattern. At best, incorrectly using the domain model pattern can lead to a waste of effort; at worst, you introduce needless complexity where a simpler implementation method would have sufficed.
在本章中你将了解的所有领域逻辑模式中,事务脚本是迄今为止最容易理解和上手的。事务脚本模式遵循过程式开发风格,而非面向对象方法。通常,每个业务事务都会创建一个单独的过程,并将其分组到某种静态管理器或服务类中。每个过程都包含完成业务事务所需的所有业务逻辑,包括工作流、业务规则、验证检查以及数据库持久化。
Of all the domain logic patterns you will read about in this chapter, transaction script is by far the easiest to understand and get up and running with. The transaction script pattern follows a procedural style of development rather than an object-oriented approach. Typically a single procedure is created for each of your business transactions, and it is grouped in some kind of static manager or service class. Each procedure contains all the business logic that is required to complete the business transaction from the workflow, business rules, and validation checks to persistence in the database.
图 5.5显示了事务脚本模式的图形表示。
Figure 5.5 shows a graphical representation of the transaction script pattern.
图 5.5交易脚本模式。
FIGURE 5.5 The transaction script pattern.
图 5.6展示了实现事务脚本模式的接口示例签名。这两个实现分别包含了处理创建拍卖和竞价业务所需的所有逻辑,包括数据访问和持久化逻辑、授权、事务并发性和一致性问题。
Figure 5.6 shows the example signature of an interface that is implementing the transaction script pattern. The two implementations contain all of the logic they require to handle the business cases of creating an auction and bidding on an auction, respectively, including data access and persistence logic, authorization, transactional concurrency, and consistency concerns.
图 5.6事务脚本模式 UML。
FIGURE 5.6 The transaction script pattern UML.
事务脚本模式的优势之一是它易于理解,即使新团队成员不具备该模式的先验知识,也能快速上手。随着新需求的出现,可以轻松地向类中添加更多方法,而不必担心影响或破坏现有功能。
One of the strengths of the transaction script pattern is that it’s simple to understand and can be fast to get new team members up to speed without prior knowledge of the pattern. As new requirements arise, it is easy to add more methods to the class without fear of impacting or breaking existing functionality.
继续讨论在线拍卖领域,请考虑清单 5-2。交易脚本代表了拍卖竞价的用例。
Continuing with the online auction domain, consider Listing 5-2. The transaction script represents the use case of bidding on an auction.
如您所见,整个业务用例都封装在一个方法中。该类负责处理许多任务,例如数据检索和持久化、事务管理以及完成投标所需的业务逻辑。
As you can see, the entire business case is encapsulated within a single method. The class is dealing with many responsibilities such as data retrieval and persistence, transaction management, as well business logic to fulfill the place of a bid.
事务脚本是一种简单的过程模式,适用于领域中逻辑很少或没有逻辑的部分。操作的所有逻辑都包含在一个服务方法中。任何开发人员都能快速掌握用于建模领域逻辑的架构。因此,对于由不熟悉面向对象编程概念的初级开发人员组成的团队来说,这是一种非常有用的模式。但是,如果逻辑变得复杂,事务脚本模式很快就会变得难以管理,因为其本质上很容易出现重复。如果出现过多的重复,请根据领域模型模式重构代码。
Transaction script is a simple procedural pattern that is useful for the parts of your domain that have little or no logic. All logic for an operation is contained within a single service method. Any developer can quickly come to grips with the architecture used to model domain logic. Therefore it is a helpful pattern for teams with junior developers who are not comfortable with object-oriented programming concepts. However, if logic becomes complex, the transaction script pattern can quickly become hard to manage because, by its nature, duplication can occur quickly. If excessive duplication occurs, refactor the code toward the domain model pattern.
当应用程序规模扩大、业务逻辑复杂度增加时,事务脚本模式的问题就暴露无遗。随着应用程序的扩展,方法的数量也会随之增加,导致 API 中充斥着功能重叠且细粒度的方法,从而变得毫无用处。您可以使用子方法来避免重复的代码(例如验证和业务规则),但工作流中的重复是不可避免的,并且随着应用程序的增长,代码库很快就会变得笨重且难以管理。
The problems with the transaction script pattern are revealed when an application grows and the business logic complexities increase. As an application is extended, so is the mass of methods, making for an unhelpful API full of fine-grained methods that overlap in terms of functionality. You can use sub methods to avoid repetitive code such as the validation and business rules, but duplication in the workflow cannot be avoided, and the code base can quickly become unwieldy and unmanageable as the application grows.
表模块模式将对象模型映射到数据库模型。单个对象代表数据库中的一个表或视图。该对象负责所有持久化需求以及业务逻辑行为。此模式的优点是对象模型和数据库模型之间没有不匹配的情况。表模块模式非常适合数据库驱动设计,因此乍一看它可能不太适合 DDD。但是,对于领域中由有界上下文隔离且仅仅是数据形式化的简单部分,此模式非常适合,并且比领域模型模式更容易掌握。但是,如果对象模型和数据库模型开始出现分歧,则需要重构到领域模型模式。
The table module pattern maps the object model to the database model. A single object represents a table or view in the database. The object is responsible for all persistence needs along with business logic behavior. The benefit of this pattern is that there is no mismatch between the object model and the database model. The table module pattern is a great fit for Database-Driven Design, so on first glance it might not be a good fit for DDD. However, for simpler parts of the domain that are isolated by a bounded context and that are simply forms over data, this pattern is a good fit and easier to come to grips with than the domain model pattern. If, however, the object model and database model start to diverge, you need to refactor toward the domain model pattern.
活动记录是表模块模式的一种变体,它将对象映射到表的行,而不是用对象本身来表示表。对象表示处于临时状态或正在修改的数据库行(记录)。
Active record is a variation of the table module pattern that maps objects to rows of a table as opposed to having objects represent the tables themselves. An object represents a database row (record) in a transient state or under modification.
活动记录模式是一种流行的模式,当你的底层数据库模型与你的业务模型相匹配时,它尤其有效。通常,数据库中的每个表都有一个业务对象。业务对象代表该表中的一行,包含数据和行为、持久化数据的方法、添加新实例和查找对象集合的方法。
The active record pattern is a popular pattern that is especially effective when your underlying database model matches your business model. Typically, a business object exists for each table in your database. The business object represents a single row in that table and contains data and behavior, as well as a means to persist it, and methods to add new instances and find collections of objects.
在活动记录模式中,每个业务对象负责自己的持久性和相关的业务逻辑。
In the active record pattern, each business object is responsible for its own persistence and related business logic.
活动记录模式非常适合数据模型与业务模型之间具有一对一映射的简单应用程序,例如博客或论坛引擎;如果您已有数据库模型或倾向于以“数据优先”的方式构建应用程序,那么它也是一个很好的选择。由于业务对象与数据库中的表具有一对一映射,并且都具有相同的创建、读取、更新和删除 (CRUD) 方法,因此可以使用代码生成工具自动生成业务模型。优秀的代码生成工具还会内置所有数据库验证逻辑,以确保只允许有效数据进行持久化。
The active record pattern is great for simple applications that have one-to-one mapping between the data model and the business model, such as with a blogging or a forum engine; it’s also a good pattern to use if you have an existing database model or tend to build applications with a “data first” approach. Because the business objects have a one-to-one mapping to the tables in the database and all have the same create, read, update, and delete (CRUD) methods, it’s possible to use code generation tools to auto-generate your business model for you. Good code generation tools also build in all the database validation logic to ensure that you are allowing only valid data to be persisted.
贫血领域模型有时被称为反模式。乍一看,该模式与领域模型非常相似,因为您仍然会找到代表业务领域的领域对象。然而,任何行为都不包含在领域对象中。相反,它位于模型之外,领域对象只是简单的数据传输类。该模式的主要缺点是,领域服务承担了一种更具过程性的代码风格,类似于您在本章开头看到的事务脚本模式,这带来了与之相关的问题。其中一个问题就是违反了“告诉,不要询问”原则,该原则规定对象应该告知客户端它们能做什么、不能做什么,而不是暴露属性,让客户端自行判断对象是否处于特定状态以执行特定操作。领域对象被剥离了逻辑,仅仅是数据容器。
The anemic domain model is sometimes referred to as an anti-pattern. At first glance, the pattern is very similar to the domain model in that you will still find domain objects that represent the business domain. Any behavior, however, is not contained within the domain objects. Instead, it is found outside of the model, leaving domain objects as simple data transfer classes. The major disadvantage of this pattern is that the domain services take on the role of a more procedural style of code rather like the transaction script pattern that you saw at the beginning of the chapter, which brings along the issues associated with it. One such issue is the violation of the “Tell, Don’t Ask” principle, which states that objects should tell the client what they can or can’t do rather than exposing properties and leaving it up to the client to determine if an object is in a particular state for a given action to take place. Domain objects are stripped of their logic and are simply data containers.
对于逻辑较少的领域模型部分,或者不太熟悉面向对象编程技术的团队,贫血领域模型模式是一个不错的选择。贫血领域模型可以整合 UL,是尝试创建丰富领域模型的良好开端。
The anemic domain model pattern is a good candidate for parts of your domain model that have little logic or for teams not very experienced with object-oriented programming techniques. The anemic domain model can incorporate the UL and be a good first step when trying to create a rich domain model.
领域驱动设计完全适用于那些偏爱函数式编程而非面向对象编程的开发者。领域模型可以轻松使用函数式概念(例如不变性和引用透明性)构建。行为丰富的对象并非必需,将状态隔离在行为接口之后也并非必需。因此,贫血领域模型模式实际上是函数式编程中一个非常有用的概念,而不是一种反模式。
Domain-Driven Design is fully accessible to developers who prefer functional to object-oriented programming. Domain models can easily be built using functional concepts like immutability and referential transparency. Behavior-rich objects are not a necessity, nor is isolating state behind behavioral interfaces. Accordingly, the anemic domain model pattern is actually a fundamentally useful concept when using functional programming as opposed to being an anti-pattern.
领域模型的存在是为了方便与领域专家进行对话,而贫血领域模型模式却阻碍了将领域概念表示为对象,这看似矛盾。然而,正如许多现代 DDD 从业者所言,最重要的领域概念是动词——不是像银行账户这样的名词,而是像转账这样的动词。借助函数式编程和贫血领域模型,您仍然可以充分表达领域动词,从而与领域专家进行有意义的对话。
It may seem contradictory that domain models are there to facilitate conversations with domain experts, and yet the anemic domain model pattern precludes the ability to represent domain concepts as objects. However, as many modern DDD practitioners assert, the most important domain concepts are verbs—not the nouns like a bank account, but the verbs like transferring funds. With functional programming and the anemic domain model, you still have the power to fully express domain verbs, and consequently to have meaningful conversations with domain experts.
在构建功能性领域模型时,即使使用贫血领域模型模式,仍然可以拥有表示领域概念的结构。但值得注意的是,它们只是没有行为的数据结构——因此是一个行为丰富、面向对象的BankAccount实体,如清单 5-3 所示。
When building functional domain models, it is still possible to have structures that represent domain concepts, even when using the anemic domain model pattern. Significantly, though, they are just data structures with no behavior—so a behavior-rich, object-oriented BankAccount entity, as shown in Listing 5-3.
将被建模为一个纯粹的、不可变的日期结构,如清单 5-4 所示:
Would be modeled as a pure, immutable date structure, as in Listing 5-4:
将对象简化为纯数据结构后,行为将以纯函数的形式存在,而挑战在于如何根据概念领域模型将它们紧密地分组和组合。一个有效的方法是将函数分组为聚合。函数的另一个重大区别在于它们的结构和职责。由于函数式编程要求不变性,因此函数需要返回更新的数据结构,而不是改变现有对象的状态。例如,面向对象的ShoppingBasket可以在客户每次添加产品时直接更新其Items集合,如清单 5-5 所示。
Having reduced objects into pure data structures, behavior then exists as pure functions, and the challenge is to cohesively group and combine them aligned with the conceptual domain model. One effective option is to group functions into aggregates. The other big divergence with your functions is their structure and responsibility. Since functional programming necessitates immutability, your functions need to return updated data structures rather than mutating the state of existing objects. For example, an object-oriented ShoppingBasket may directly update its Items collection each time the customer adds a product, as seen in Listing 5-5.
采用功能方法,不是更新Items集合,而是返回ShoppingBasket的副本,其中包含更新的、不可变的Items集合,如清单 5-6 所示。
Taking the functional approach, instead of updating the Items collection, a copy of the ShoppingBasket is returned that contains an updated, immutable Items collection, as seen in Listing 5-6.
请注意,Items集合不再能够被封装,因为函数无法访问ShoppingBasket的私有状态。
Notice the Items collection can no longer be encapsulated since functions cannot access the ShoppingBasket’s private state.
一些编程语言,包括 Haskell、Scala 和 Clojure,将函数式编程作为首要特性。但在传统的面向对象语言(如 C# 和 Java)中,仍然可以构建函数式领域模型。
Some programming languages, including Haskell, Scala, and Clojure, make functional programming a first-class feature. But it is still possible to build functional domain models in traditionally object-oriented languages like C# and Java.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
在大型复杂应用程序中,您会发现多个模型同时发挥作用。每个模型都将构建为代表问题域的不同部分,每个实现都使用适合问题复杂性的代码设计模式。理想情况下,每个子域都应该有一个模型;然而,情况并非总是如此,因为某些复杂的子域可能包含多个模型,而某些模型可能跨越两个或多个子域。无论您拥有多少个模型,您都会发现它们需要交互才能实现系统的行为。如果团队在组合模型时,没有清晰地了解它们的应用场景,那么由于概念和逻辑的交织,模型很容易变得模糊,失去明确性。
In large and complex applications you will find multiple models at play. Each model will be built to represent a distinct area of the problem domain, with each implementation using an appropriate code design pattern suitable for the complexity of the problem. Ideally you will have a model for each subdomain; however, this might not always be the case as some complex subdomains could contain more than a single model and some models could span two or more subdomains. No matter how many models you have you will find that they will need to interact to fulfill the behaviors of a system. It is when models are combined by teams without a clear understanding of what context they apply to that they are prone to become blurred and lose explicitness, as concepts and logic are intermingled.
因此,保护每个模型的完整性并在代码中清晰定义其职责边界至关重要。这可以通过将模型绑定到特定上下文(称为有界上下文)来实现。有界上下文是根据团队的语言和物理工件定义的。有界上下文使模型保持一致且有意义,这对于管理解决方案空间的复杂性至关重要。认真使用有界上下文对于成功实施领域驱动设计至关重要。
Therefore it is vital to protect the integrity of each model and clearly define the boundaries of their responsibility in code. This is achieved by binding a model to a specific context, known as a bounded context. A bounded context is defined based on team’s language, and physical artifacts. Bounded contexts enable a model to stay consistent and meaningful, which is vital in managing complexity in the solution space. Diligent use of bounded contexts is essential to being successful with Domain-Driven Design.
领域驱动设计的核心在于需要在代码中创建清晰、可演化的模型,并与共享的概念模型保持一致。随着新的领域洞察的获得,它们可以有效地融入到模型中。然而,如果对整个系统使用单一模型,模型中某个领域的概念可能会与系统中另一个领域中发音相似的概念混淆,甚至相互耦合。因此,DDD 提倡将大型复杂系统分解为多个代码模型。
At the core of Domain-Driven Design is the need to create explicit, evolvable models in code that align with the shared conceptual models. As new domain insights are gained, they can be incorporated into the model efficiently. However, if a single model is used for an entire system, concepts from one area of the model can be confused with similar-sounding concepts from another area of the system—and even become coupled to them. Therefore, DDD advocates that you break up a large complex system into multiple code models.
大型模型涵盖众多领域概念,并执行众多业务用例。因此,很容易犯错,并将错误的概念归为一类。此外,找到所需内容也会非常困难。系统规模越大,这些问题就越严重,从而减慢了添加新功能和改进的速度。
Large models accommodate many domain concepts and carry out many business use cases. As a consequence, it is easy to make mistakes and group the wrong concepts together. It can also be very difficult to find what you are looking for. The more the system grows, the more severe these problems become, slowing down the speed at which new features and improvements can be added.
如图6.1所示,随着每个新用例和见解纳入模型,模型中的概念和依赖关系的数量会增加,从而导致复杂性增加。
As Figure 6.1 highlights, with each new use case and insight incorporated into the model, the number of concepts and dependencies in the model grows, resulting in increased complexity.
图 6.1模型的复杂性将会增加。
FIGURE 6.1 A model will grow in complexity.
代码复杂只是单一模型带来的问题之一。协作开销和组织效率低下也是单片模型可能引发的主要问题。
Complex code is just one of the problems arising from a single model. Collaboration overhead and organizational inefficiencies are also major problems a monolithic model is likely to cause.
当一个团队想要发布新功能时,他们必须与其他团队确认他们的更改是否也能部署。第一个团队要么必须等待,要么必须使用复杂的分支策略。正如第 11 章“有界上下文集成简介”更详细地解释的那样,复杂的分支策略可能会严重阻碍组织频繁高效地交付业务价值并了解客户的能力。
As one team wants to release a new feature, they have to check with other teams that their changes can also be deployed. Either the first team will have to wait, or complex branching strategies will be used. As Chapter 11, “Introduction to Bounded Context Integration,” explains in more detail, complex branching strategies can be a big hindrance to an organization’s ability to frequently and efficiently deliver business value and learn about their customers.
不断要求团队协作设计新功能或计划发布版本是不必要的低效行为。由于每个团队都与各自的领域专家合作,并试图将模型推向不同的方向,因此拖累其他团队会造成相互浪费。团队越多,协作成本就越高,代码库也就越复杂,如图6.2所示。
Continually requiring teams to collaborate on the design of new features or to plan releases is an unnecessary inefficiency. As each team works with their own domain expert and tries to drive their model in different directions, dragging other teams along with them is mutually wasteful. The more teams, the more expensive the collaboration overhead, and the more complex the codebase, as Figure 6.2 illustrates.
图 6.2模型的复杂性随着多个团队的增加而增加。
FIGURE 6.2 Complexity in a model increases with multiple teams.
如果使用多个模型,团队可以迭代他们的模型并频繁有效地提供新价值,因为他们不必与其他团队同步或关注其他团队模型中的概念。
If multiple models are used instead, teams can iterate on their models and deliver new value frequently and efficiently because they do not have to synchronize with other teams or concern themselves with concepts from other teams’ models.
您可能会担心团队在每个模型中重复代码。但请关注消除团队间依赖关系所带来的好处。本质上,模型间重复代码是可以的,因为概念并不相同。
You may be concerned about a team’s duplicating code in each of their models. But focus on the benefits that arise by removing dependencies between teams. Essentially, it is Ok to duplicate code between models because the concepts are not the same.
DDD 从业者的一个顿悟是,他们意识到系统中的一些概念非常相似——它们甚至可能同名。然而实际上,它们对业务的不同部门而言含义截然不同。如图6.3所示,“工单”概念对销售部门和客服部门而言含义不同。
One of the epiphanies that DDD practitioners have is the realization that some concepts in a system are very similar—they might even have the same name. Yet actually, they mean very different things to different parts of the business. As Figure 6.3 illustrates, the “Ticket” concept means different things to the Sales and Customer Service departments.
图 6.3领域术语在不同语境下具有不同含义。
FIGURE 6.3 Domain terms mean different things in different contexts.
一旦你接受了名称在不同语境下可能具有不同含义这一事实,你就更容易接受多个小型模型比单个大型模型更有效。你也能与领域专家进行更有意义的讨论。例如,根据图 6.3,当你与销售经理讨论门票时,你知道她关心的是活动的费用和地点;而与客服经理讨论门票时,则会关注客户提出的问题的严重性和类别。
Once you accept that names can have different meanings in different contexts, it’s easier to accept that multiple smaller models are more effective than a single large one. You can also have more meaningful discussions with domain experts. For example, based on Figure 6.3, when talking to the Sales manager about tickets, you know she cares about the cost and location of an event; whereas discussion about tickets with the Customer Service manager will be focused on the severity and category of problems raised by customers.
有时,问题域中的单个物理实体可能会被错误地归类为代码中的单个概念。当物理实体实际上代表多个概念,并且每个概念在不同上下文中具有不同含义时,这种情况就会很麻烦。典型的例子就是产品。
Sometimes, a single physical entity in the problem domain can mistakenly be classified as a single concept in code. This is problematic when the physical entity actually represents multiple concepts, that each mean different things in different contexts. The classic example is a product.
图 6.4展示了产品在不同情境下的含义。对于采购团队来说,产品是一个必须以盈利利润和可接受的交付周期才能获得的概念。然而,对于销售团队来说,产品只是一个包含图片、尺码指南并属于某个销售类别的概念——这些都与采购团队无关,尽管在问题领域中,它们实际上是同一个物理实体。
Figure 6.4 shows how products mean different things in different contexts. It is a concept that must be acquired with a profitable margin and acceptable lead time to the Procurement team. Yet to the Sales team a product is a concept with images, size guides, and belongs to a selling category—none of which are relevant to the Procurement team, even though it is the same physical entity in the problem domain.
图 6.4相同的概念应该在不同的语境中理解。
FIGURE 6.4 The same concept should be understood within different contexts.
当一个物理实体(例如产品)实际上代表多个领域概念时,开发人员通常会将其建模为单个概念。不幸的是,我们很容易陷入这样的思维陷阱:因为产品可以是物理实体,所以在代码中也应该将其建模为单个类。这会导致耦合,因为每个模型都共享同一个产品类,如图6.5所示。
When a physical entity, such as a product, actually represents multiple domain concepts, it is often modeled as a single concept by developers. Unfortunately, it’s very easy to fall into the trap of thinking that because a product can be a physical item that it should be modeled as a single class in code. This leads to coupling, as each model shares the same product class, as shown in Figure 6.5.
图 6.5域中所有子域的实体的单一视图很快就会成为一个问题。
FIGURE 6.5 A single view of an entity in the domain for all subdomains can quickly become a problem.
如前所述,当多个上下文耦合时,代码可能会变得过于复杂,团队之间的协作成本也会变得非常高昂。在这个示例产品中,共享类也违反了单一职责原则 (SRP),因为四个上下文都出于完全不同的原因而希望它进行更改。
As discussed previously, when multiple contexts are coupled, code can become excessively complex and the collaboration overhead between teams can become excessively costly. The shared class, in this example product, also violates the Single Responsibility Principle (SRP), since there are four contexts that all want it to change for completely different reasons.
当代码中没有边界时,很容易产生耦合,就像图 6.5所示的产品一样。一个更好的降低耦合的解决方案是为每个上下文(促销、分配、忠诚度和运输)设置各自的模型。这样,每个模型都将包含一个唯一的产品表示,该表示仅满足模型上下文的需求。图 6.6展示了共享Product类的多项职责,并指明了它们各自应该属于哪个模型。
When there are no boundaries in the code, it is too easy for coupling to occur, as with the product shown in Figure 6.5. A better solution that reduces the coupling would be for each context—Promotion, Allocation, Loyalty, and Shipping—to have its own model. Each model would then contain a unique representation of a product that only satisfies the needs of the model’s context. Figure 6.6 shows the multiple responsibilities of the shared Product class, indicating which model each of them should really belong in.
图 6.6产品,上帝对象反模式的实现。
FIGURE 6.6 The product, an implementation of the god object antipattern.
选择较小模型的另一个原因是,与遗留代码或第三方集成的问题更少。当有大量遗留代码时,向单体代码库添加新功能可能会非常痛苦。您希望添加与领域专家共同创建的简洁、全新且富有洞察力的模型,但遗留代码的局限性可能会限制设计的表达能力。但是,如果您拥有较小的模型,则并非所有模型都需要接触遗留代码。
Another reason to prefer smaller models is that integrating with legacy code or third parties can be less problematic. Adding new features to a monolithic codebase can be painful when there is lots of legacy code. You want to add clean, new, insightful models that you created with domain experts, but the limitations of legacy code can constrain the expressiveness of your design. But if you have smaller models, not all of them will need to touch the legacy code.
第 11 章讨论的许多模式展示了当您有多个较小的模型可供使用时如何更轻松地将 DDD 应用于遗留系统。
A number of patterns, discussed in Chapter 11, show how it is easier to apply DDD to legacy systems when you have multiple smaller models to work with.
在某些情况下,拥有一个涵盖整个系统的单一模型很有用,包括业务信息 (BI) 和报告。然而,企业模型并非创建可演进、能够明确表达领域概念的领域模型的最佳解决方案。企业模型也不适合旨在频繁交付业务价值的迭代开发流程。
Having a single model of the entire system is useful in some scenarios, including business information (BI) and reporting. However, the enterprise model is not the best solution for creating an evolvable domain model that explicitly expresses domain concepts. Nor is an enterprise model suitable for iterative development processes that aim to deliver business value frequently.
图 6.7展示了如何兼顾两方面的优势——针对每种情况的独特模型和针对 BI 的企业模型。
Figure 6.7 shows how you can have the best of both worlds—a unique model for each context and an enterprise model for BI.
图 6.7企业模型和领域模型之间的区别。
FIGURE 6.7 The difference between an enterprise model and a domain model.
有界上下文定义了模型的适用性。它明确了模型的用途、模型应在何处保持一致以及应忽略哪些内容。有界上下文确保模型上下文之外的领域概念不会分散模型设计初衷的注意力。解决。有界上下文可以让团队明确了解模型负责什么和不负责什么。
A bounded context defines the applicability of a model. It gives clarity on what a model is used for, where it should be consistent, and what it should ignore. A bounded context ensures that domain concepts outside a model’s context do not distract from the problem it was designed to solve. A bounded context makes it explicit to teams what the model is responsible for and what it is not.
上下文是领域驱动设计中的一个重要术语。每个模型都有一个在子域中隐式定义的上下文。当你在履行子域的上下文中谈论一个产品时,你不必称它为可履行的产品;同样,在购物的上下文中谈论它时,它也不是可销售的产品。它只是一个定义在上下文中的产品。
Context is an important term in Domain-Driven Design. Each model has a context implicitly defined within a subdomain. When you talk about a product in the context of the fulfillment subdomain, you don’t need to call it a product that can be fulfilled; likewise, when talking in the context of shopping, it’s not a saleable product. It’s simply a product in a defined context.
在与领域专家或开发团队的其他成员沟通时,您应该确保每个人都了解您所谈论的背景。背景定义了模型的范围,限制了问题空间的边界,使团队能够集中注意力而不受干扰。
When communicating with domain experts or other members of the development team, you should ensure that everyone is aware of the context you are talking in. The context defines the scope of the model, limiting the boundaries of the problem space, enabling the team to focus without distractions.
在第 4 章“模型驱动设计”中,我们将介绍通用语言 (UL) 的概念,以及在避免语言歧义的上下文中定义的模型的重要性。上下文指的是模型的特定职责,有助于分解和组织问题空间。有界上下文通过将模型封装在职责边界内,进一步拓展了“上下文中的模型”的概念。此边界是一种具体的技术实现,与更抽象的上下文不同。有界上下文以不降低模型纯度的方式强制通信。
In Chapter 4, “Model-Driven Design,” you are introduced to the concept of the ubiquitous language (UL) and the importance of models defined in a context that are free from linguistic ambiguity. The context refers to the specific responsibility of the model, which helps to decompose and organize the problem space. A bounded context takes the idea of a model in context further by encapsulating it within a boundary of responsibility. This boundary is a concrete technical implementation, as opposed to the context that is more abstract. The bounded context enforces communication in such a manner as to not lessen the purity of the model.
有界上下文首先是语言边界。当你与领域专家交谈时,如果你觉得某个句子需要上下文,这很大程度上暗示你需要将该模型隔离在一个有界上下文中。
A bounded context is first and foremost a linguistic boundary. When talking with domain experts, if you feel a sentence requires a context, this is a big hint that you need to isolate that model within a bounded context.
图 6.8将术语置于上下文中并识别多种模型。
FIGURE 6.8 Putting terms into context and identifying multiple models.
图 6.9展示了产品在应用于特定上下文时如何成为一个更小、更专注的概念。在开发应用程序时,务必将模型隔离在有界上下文中,以避免职责模糊导致代码类似于 BBoM。
Figure 6.9 shows how a product can be a smaller more focused concept when applied to a specific context. It is important when developing the application that you isolate models within bounded contexts to avoid the blurring of responsibilities that can lead to code that resembles a BBoM.
图 6.9在每个模型的上下文中定义它。
FIGURE 6.9 Define each model within its own context.
在大型系统中,有界上下文的必要性显而易见,但识别有界上下文及其边界的过程却充满挑战。幸运的是,这并不是一个必须事先做出完全正确的决定。随着您对领域了解的加深,您可以调整有界上下文的边界。
The need for bounded contexts is clear in larger systems, but the process of identifying bounded contexts and their boundaries is challenging. Fortunately, it’s not an up-front decision you have to get perfectly correct. As you learn more about the domain, you can adjust the boundaries of your bounded contexts.
问题域的两个方面可以作为识别有界上下文的指南——术语和业务能力。正如您在本章前面所看到的,同一个术语在不同的上下文中可能具有不同的语义。如果您可以根据单词或短语含义的变化来描绘领域模型,那么您很可能已经确定了有界上下文的边界。业务能力通常很容易辨别,但也可能具有误导性。例如,如果一个企业有一个销售部门和一个客户服务部门,那么很可能存在销售和客户有界上下文。但这并非总是如此,因此重要的是不要盲目地对业务能力进行建模。
There are two aspects of a problem domain that you can use as a guide to identifying bounded contexts—terminology and business capabilities. As you’ve seen previously in this chapter, the same term can have different semantics in different contexts. If you can delineate a domain model based on a change in the meaning of a word or phrase, you will very likely have identified the boundary of a bounded context. Business capabilities are often easy to discern but can be misleading. For example, if a business has a Sales department and a Customer Service department, there is very likely to be a sales and customer bounded context. But that’s not always true, so it’s important not to blindly model business capabilities.
在问题领域之外,团队结构和位置也会对上下文边界产生很大影响,与遗留系统或第三方系统的集成也会对上下文边界产生很大影响。
Outside of the problem domain, team structure and location can also be a big influence on context boundaries, as can integrating with legacy or third-party systems.
然而,大小并非界定有界上下文的准则。没有绝对或相对的值可以告诉你需要多少个类或多少行代码。有界上下文的大小主要取决于问题域的各个方面。因此,有些有界上下文可能很大,而有些则很小。
Size, though, is not a guideline for delineating bounded contexts. No absolute or relative value can tell you how many classes or lines of code you need. A bounded context’s size is dependent mostly on aspects of the problem domain. Some bounded contexts may, therefore, be large while others are small.
总而言之,上下文边界可能受到以下因素的影响:
To summarize, context boundaries can be influenced by the following:
与领域专家交谈时,明确说明所使用的上下文非常重要,因为术语在不同的上下文中可能具有不同的含义。正如本章所重复的那样,您的领域中将有多种模型发挥作用。您需要强制执行语言边界以保护领域术语的有效性。因此,语言边界是有界上下文边界。如果产品概念在同一个模型中具有多种含义,则应将该模型拆分为至少两个有界上下文,每个上下文对产品概念只有一个定义。这在前面已经讨论过,并在图 6.11中进行了说明。同样,同一个术语可以指代多个概念,就像图 6.3所示的票证示例一样。这也是语言边界的一个例子,它应该是有界上下文的边界。
It’s important to be explicit about what context you’re using when talking with domain experts, because terminology can have different meanings in different contexts. As repeated throughout this chapter, multiple models will be at play in your domain. You need to enforce linguistic boundaries to protect the validity of a domain term. Therefore, linguistic boundaries are bounded context boundaries. If the concept of a product has multiple meanings inside the same model, then the model should be split into at least two bounded contexts, each having a single definition of the product concept. This was discussed previously and illustrated in Figure 6.11. Equally, the same term can refer to multiple concepts, as was the case with the ticket example illustrated in Figure 6.3. That is also an example of a linguistic boundary that should be the boundary of a bounded context.
组织是一个由相互依存的服务组成的生态系统,每个服务都有各自的词汇。因此,业务能力通常是语言界限的有力指标。如前所述,销售部门和客户服务部门对工单概念的定义可能完全不同。因此,您应该将业务能力视为潜在的上下文界限。
An organization is an ecosystem of interdependent services, each with its own vocabulary. Hence business capabilities are often strong indicators of linguistic boundaries. As mentioned previously, a Sales department and Customer Service department can have completely different definitions of a ticket concept. Accordingly, you should look to business capabilities as potential context boundaries.
使用业务功能来描述有界上下文时要小心谨慎。有时,业务功能并不完全符合问题领域。最终,你可能会得到一个系统,它反映了组织的沟通结构,但并不能忠实地代表该领域。康威定律甚至暗示,一个系统必然会反映组织的沟通结构:
Be careful when using business capabilities to delineate bounded contexts. Sometimes business capabilities do not align perfectly with the problem domain. You can end up with a system that mirrors an organization’s communication structure, but does not faithfully represent the domain. Conway’s Law even implies that a system will inevitably reflect an organization’s communication structure:
“任何设计系统(这里的定义比信息系统更广泛)的组织都将不可避免地产生一种设计,其结构是该组织的通信结构的副本。”
“Any organization that designs a system (defined more broadly here than just information systems) will inevitably produce a design whose structure is a copy of the organization’s communication structure.”
您可以通过两种方式运用康威定律。首先,您可以充分理解康威定律,这样您就不仅仅只是对组织结构进行建模。其次,您可以根据系统所需的架构来重塑组织。这两种方法都需要付出巨大的努力,因此,在做出决定之前,请务必仔细规划。
You can use Conway’s Law as a guide in two ways. Firstly, you can be cognizant of Conway’s Law so that you don’t just model the organization’s structure. Alternatively, you can remodel your organization based on the desired architecture of your system. Either approach is going to require a big effort, so it’s not a decision you should take without careful planning.
一个团队应该负责一个有界上下文,无论该上下文跨越一个或多个应用程序或部门。因此,应围绕有界上下文构建团队;组建产品和服务小组,而不是试图照搬业务部门的架构。确保团队负责从展示到领域逻辑再到持久化的有界上下文。
A single team should be responsible for a bounded context, whether that crosses one or many applications or departments. So structure teams around bounded contexts; form product and services groups rather than trying to mirror the departmental structure of the business. Ensure that teams are responsible for a bounded context from presentation through domain logic and to persistence.
将团队与限界上下文协调一致的主要原因是,独立性可以让团队更快地行动并做出更好的决策。如果团队能够完全掌控产品和技术决策,他们就能更快地行动。如果他们不必担心影响其他团队,他们也能更快地进行迭代。
The main rationale for aligning teams with bounded contexts is that independence allows teams to both move faster and make better decisions. Teams can move faster if they are in full control of product and technical decisions. They can also iterate much more rapidly if they don’t have to worry about affecting other teams.
单个团队可以专注于其业务重点;因此,当需要做出决策或有人提出建议时,每个人都可以迅速聚在一起,共同商定最佳的推进方案。相反,不同的团队可能有不同的业务重点和安排,这会影响他们高效协作的能力。例如,一个团队可能必须等到另一个团队有空时才能安排会议,开始制定决策或迭代概念。
A single team can stay focused on its business priorities; therefore, when a decision needs to be made or someone has a suggestion, everyone can quickly huddle together and decide on the best way forward. Conversely, different teams might have different business priorities and arrangements that affect their ability to work together efficiently. For example, one team might have to wait until the other team becomes available before they can schedule a meeting and start making decisions or iterating on concepts.
记住,团队间的沟通有时是件好事,所以不要完全避免;只要在有用的时候进行就行。知识和技能共享就是跨团队沟通的一个例子。
Remember, communication between teams is sometimes a good thing, so don’t completely avoid it; just limit it to when it’s useful. One example of useful cross-team communication is knowledge and skill sharing.
虽然拥有完全独立的团队可以提高生产力,但为了共享知识和技能,确保团队之间仍然保持沟通至关重要。最终,有界上下文会在运行时结合起来执行完整的用例,因此团队需要全面了解他们的有界上下文如何融入更广泛的系统。解决此问题的既定模式包括定期举行会议,让团队与其他开发团队分享他们正在进行的工作、他们的实现方式或帮助他们实现目标的任何技术。另一个很好的模式是跨团队结对编程。这需要将开发人员调到另一个团队几天,以了解该领域的相关部分。您可以基于小组会议和人员调动这两个概念,衍生出许多新颖的方法。
Although having completely independent teams is a productivity win, it’s important to ensure that communication between teams still occurs for knowledge and skill-sharing benefits. Ultimately, bounded contexts combine at run time to carry out full use cases, so teams need a big-picture understanding of how their bounded context(s) fit into the wider system. Established patterns for this problem involve having regular sessions in which teams share with other development teams what they are working on, how they have implemented it, or any technologies that have helped them achieve their goals. Another excellent pattern is cross-team pair programming. This involves moving a developer to a different team for a few days to learn about that part of the domain. You can spawn many novel approaches based on these two concepts of having group sessions and moving people around.
当需要进行重大变更时,努力确保团队高效沟通确实会有所回报。而且在每个系统中,你总会遇到这些情况。在某些时候,有界上下文之间的契约需要改变以满足业务需求。让团队沟通以制定最佳的整体解决方案有时可能是最有效的选择。
Making an effort to ensure that teams communicate efficiently really pays off when breaking changes need to occur. And in every system, you do always get them. At some point, the contract between bounded contexts needs to change to meet the needs of the business. Having teams communicate to work out the best overall solution can sometimes be the most efficient option.
图表和轻量级文档有助于团队快速共享知识,尤其是在新成员加入时。您将在第七章“上下文映射”中了解上下文图和其他类型的图表如何促进知识共享。
Diagrams and lightweight documentation help teams share knowledge quickly, especially when new members join. You’ll see how context maps and other types of diagrams facilitate knowledge sharing in Chapter 7, “Context Mapping.”
为了展示情境建模的重要性,并揭示领域内的多种模型,你可以使用另一个辅助游戏。由 Greg Young 首创的“情境游戏”(http://codebetter.com/gregyoung/2012/02/29/the-context-game-2/)有助于明确哪些地方需要额外的模型才能有效地映射问题空间。
To demonstrate the importance of modeling in context and to reveal multiple models within the domain, you can employ another facilitating game. The Context Game, pioneered by Greg Young (http://codebetter.com/gregyoung/2012/02/29/the-context-game-2/), helps to make it clear where an additional model is required to map the problem space effectively.
当你觉得某个术语过于繁琐或含糊不清时,可以在知识研讨环节引入这个游戏。将团队分成几个小组,由开发人员和业务专家组成。你应该按部门或业务职责对业务专家进行分组。给他们20分钟时间,让他们定义这个术语或概念在他们各自业务部门的含义,并让开发人员来掌握这些知识。然后,召集整个团队,讨论他们对这个概念的看法。
You can introduce the game into knowledge-crunching sessions when you think you have an overloaded or ambiguous term. Split the group into smaller groups of developers and business experts. You should split the business experts by department or business responsibility. Give them 20 minutes to come up with a definition of what the term or concept means to them in their part of the business, using the developers to capture the knowledge. Then bring the whole team together to present their views on the concept.
你会发现,业务的不同部门对共享术语有不同的看法。当业务职能部门意见分歧时,你就需要划定上下文界限,并创建一个新的模型。如图6.8所示,产品概念存在于许多不同的上下文中。
You will find that different parts of the business have different views on the shared terminology. Where the business functions have a difference of opinion is where you need to draw your context lines and create a new model. This was shown in Figure 6.8 with the product concept existing in many different contexts.
子域,如第三章“聚焦核心域”所述,代表问题域的逻辑区域,通常反映业务组织结构的业务能力。它们用于区分应用程序中的重要区域(核心域)和不太重要的区域(支持域和通用域)。子域的存在是为了提炼问题空间并分解复杂性。
Subdomains, introduced in Chapter 3, “Focusing on the Core Domain,” represent the logical areas of a problem domain, typically reflecting the business capabilities of the business organizational structure. They are used to distinguish the areas of importance in an application, the core domain, from the less important areas, the supporting and generic domains. Subdomains exist to distill the problem space and break down complexity.
领域模型的构建是为了满足每个子领域的用例。理想情况下,模型和子领域之间应该存在一对一的映射,但情况并非总是如此。模型的定义基于团队结构、语言的歧义性、业务流程的一致性或物理部署。因此,一个子领域可能包含多个模型,而一个模型也可能跨越多个子领域。在传统环境中,这种情况很常见。
Domain models are built to fulfill the uses cases of each of the subdomains. Ideally there would be a one-to-one mapping between models and subdomains, but this is not always the case. Models are defined based on team structure, ambiguity in language, business process alignment, or physical deployment. Therefore a subdomain could contain more than a single model and a model could span more than a single subdomain. This is often the case within legacy environments.
为了保持纯粹性和专注性,模型需要被隔离并定义在一个明确的上下文中。正如你所了解的,这个上下文被称为有界上下文。与子域不同,有界上下文是一种具体的技术实现,它强制规定应用程序内模型之间的边界。有界上下文存在于解决方案空间中,并在上下文中表示为显式的领域模型。
Models need to be isolated and defined within an explicit context in order to stay pure and focused. As you’ve learned, this context is known as the bounded context. Unlike a subdomain, a bounded context is a concrete technical implementation that enforces boundaries between models within an application. Bounded contexts exist in the solution space and are represented as explicit domain models in a context.
有界上下文拥有从表示层、域逻辑层到持久层甚至数据存储的垂直功能片段。
A bounded context owns the vertical slice of functionality from the presentation layer, through the domain logic layer, on to the persistence, and even to the data storage.
将有界上下文的概念应用于之前图 6.5所示的系统,将得到一个系统,其中每个有界上下文负责各自的表示、领域逻辑和持久化职责,如图 6.10所示。在这个改进的架构中,产品的概念可以存在于每个有界上下文中,并且只包含该上下文中常见的属性和逻辑。任何有界上下文中的更改都不会对其他上下文产生不良影响,因为子域现在是隔离的。
Applying the concept of bounded contexts to the system shown previously in Figure 6.5 results in a system with each bounded context looking after its own presentation, domain logic, and persistence responsibilities, as shown in Figure 6.10. In this improved architecture, the concept of a product can exist in each bounded context and only contain attributes and logic prevalent to that context alone. Changes in any bounded context no longer have undesired effects on others because the subdomains are now isolated.
图 6.10每个有界上下文(而不是每个应用程序)的分层架构模式。
FIGURE 6.10 A layered architecture pattern per bounded context and not per application.
仔细观察,如图 6.11所示,可以看出产品概念存在于两个模型中,但由其所在的上下文定义。
A closer inspection, as shown in Figure 6.11, shows the product concept existing in two models but defined by the context that it is within.
图 6.11不同上下文中的 Product 类。
FIGURE 6.11 The Product class in different contexts.
并非所有有界上下文都需要共享相同的架构模式。如果一个有界上下文包含一个逻辑复杂度较低的支持领域或通用领域,您可能更倾向于采用创建、读取、更新和删除 (CRUD) 风格的开发。然而,如果领域逻辑足够复杂,最好创建一个丰富的面向对象领域模型。一旦将有界上下文分离,您就可以更进一步,应用不同的架构模式,如图6.12所示。
Not all bounded contexts need to share the same architectural pattern. If a bounded context contains a supporting or generic domain with a low logic complexity, you might want to favor a more create, read, update, and delete (CRUD) style of development. If, however, the domain logic is sufficiently complex, it’s best to create a rich object-oriented model of the domain. Once bounded contexts are separated you can go a step further and apply different architectural patterns, as shown in Figure 6.12.
图 6.12您可以将不同的架构模式应用于不同的有界上下文。
FIGURE 6.12 You can apply different architectural patterns to the different bounded contexts.
图 6.12展示了如何在应用程序的每个有界上下文中使用不同的架构模式。各种有界上下文通过复合 UI 组合在一起,呈现给用户。图 6.13显示,有界上下文封装了基础架构、数据存储、用户界面以及领域模型。
Figure 6.12 shows how you can use different architectural patterns within each bounded context of an application. The various bounded contexts are pulled together using a composite UI to display to the user. Figure 6.13 shows that the bounded context encapsulates the infrastructure, data store and user interface as well as the domain model.
图 6.13有界上下文的解剖结构。
FIGURE 6.13 The anatomy of a bounded context.
从根本上说,自治是有界上下文的一个关键特性,它将团队与外部干扰隔离开来,并将模型与不相关的概念隔离开来。在第二部分中,您将看到使用可扩展集成方法(包括基于消息传递和 REST 的事件驱动架构)实现和集成自治有界上下文的实际示例。
Fundamentally, autonomy is a key characteristic of bounded contexts that isolates teams from external distractions, and isolates models from unrelated concepts. In Part II you will see practical examples of implementing and integrating autonomous bounded contexts using scalable integration approaches, including event-driven architecture with messaging and REST.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
在大型复杂应用程序中,上下文中的多个模型会协作以满足系统的需求和行为。单个团队可能无法拥有系统的所有子组件,有些组件是现有的遗留代码,由其他团队负责,而其他组件则由第三方提供,而这些第三方对使用其功能的客户一无所知。如果团队无法充分理解系统内的不同上下文及其相互关系,则在集成有界上下文时可能会损害正在运行的模型。如果团队没有明确映射和理解上下文之间的关系,模型之间的界限可能会变得模糊,最终导致“泥球”(Big Ball of Mud)。
In large and complex applications, multiple models in context collaborate to fulfill the requirements and behaviors of a system. A single team may not own all of the various sub components of a system, some will be existing legacy code that is the responsibility of a different team, and other components will be provided by third parties that will have no knowledge of the clients that will consume its functionality. Teams that don’t have a good understanding of the different contexts within a system, and their relationships to one another, run the risk of compromising the models at play when integrating bounded contexts. Lines between models can become blurred resulting in a Big Ball of Mud if teams don’t explicitly map and understand relationships between contexts.
系统内情境的技术细节并非阻碍项目成功的唯一因素。负责情境的团队之间的组织关系也会对项目结果产生重大影响。通常,管理其他情境的团队的动机并不相同,或者他们的优先级有所不同。为了项目取得成功,团队通常需要在政治层面而非技术层面来管理这些情况下的变化。
The technical details of contexts within systems are not the only force that can hamper the success of a project. Organizational relationships between the teams that are responsible for contexts can also have a big impact on the outcome of a project. Often, teams that manage other contexts are not motivated by the same forces, or they have different priorities. For projects to succeed, teams usually need to manage changes in these situations at a political rather than technical level.
开发过程中还可能出现其他非技术挑战。这些问题源于问题域中未明确定义的限界上下文之间的区域。这些重要的业务流程通常不受开发团队和业务负责人的职责约束,但矛盾的是,它们对业务工作流和流程却至关重要。
Other nontechnical challenges can appear during development. These are issues that arise from the areas of the problem domain that sit between bounded contexts that have not been explicitly defined. These important business processes can often be devoid of responsibility from development teams and business ownership, but paradoxically are immensely important to business workflows and processes.
为了应对这些挑战,团队可以创建上下文图来捕捉各种有界上下文之间的技术和组织关系。上下文图的最大优势在于,它能够捕捉到实际情况,包括所有缺陷,而不是过时的高层设计文档。不断发展的上下文图确保团队能够了解系统的整体视图,包括技术和组织层面,从而使他们能够尽早解决问题,并避免因破坏模型的完整性而意外削弱其实用性。
To combat these challenges, teams can create context maps to capture the technical and organizational relationships between various bounded contexts. The greatest strength of a context map is that it is used to capture the reality of the landscape, warts and all, as opposed to an outdated high-level design document. The context map, ever evolving, ensures that teams are informed of the holistic view of the system, both technical and organizational, enabling them to have the best possible chance of overcoming issues early and to avoid accidentally weakening the usefulness of the models by violating their integrity.
如图 7.1所示,上下文图是一个重要的工件;它的职责是确保明确定义系统各个上下文之间的边界,并确保每个团队都了解它们之间的联系点。上下文图不是用某种企业架构工具创建的非常详细的文档,而是一个高级的手绘图表,可以传达所涉及上下文的整体情况。上下文图应该足够简单,以便领域专家和开发团队都能理解。除了清楚地标记团队理解的上下文之外,该图还应显示系统中不太容易理解的区域,以反映代码库混乱且通常难以理解的现实。
A context map, as shown in Figure 7.1, is an important artifact; its responsibility is to ensure that boundaries between various contexts of the system are defined explicitly and that each team understands the contact points between them. A context map is not a highly detailed document created in some kind of enterprise architecture tool, it is a high-level, hand drawn diagram that communicates a holistic picture of the contexts in play. A context map should be simple enough to be understood by domain experts and development teams alike. As well as clearly labelling the contexts the teams understand, the diagram should also show areas of the system that are not well understood to reflect the messy and often unintelligible reality of the codebase.
图 7.1上下文图。
FIGURE 7.1 A context map.
如图 7.2所示,该图的技术细节展示了上下文之间的集成点。这张热力图对于团队理解其变更的技术含义至关重要。它显示了现有的边界以及用于维护有界上下文完整性的任何转换。
The technical details of the map, as shown in Figure 7.2 demonstrate the integration points between contexts. This heat map is vital for teams to understand the technical implications of their changes. It shows the boundaries that exist and any translations that are used to retain the integrity of bounded contexts.
图 7.2上下文图上的技术集成。
FIGURE 7.2 The technical integration on a context map.
极其重要的是,上下文图要反映现实,展示代码的当前状态,而不是理想的未来状态。上下文图不必展示模型的细节;相反,它们必须展示有界上下文之间的集成点和数据流。与代码模型和分析模型一样,上下文图应该只在代码发生变化时才进行调整,这样才不会给人留下错误的印象。上下文图应该展现真实的现实;只有这样,它才有用。
It is extremely important that context maps reflect reality, showing the code in the present state rather than an ideal future state. Context maps need not show the detail of a model; instead, they must demonstrate the integration points and the flow of data between bounded contexts. Like the code model and analysis model, the context map should change only when code changes so it does not give a false impression of the landscape. The map should show the stark reality; only then will it be useful.
业务流程的变更或新工作流程的创建通常会跨越多个有界上下文,并影响到领域内的各个部分。协调这种规模的变更通常需要与技术变更一样多的团队管理。了解每个需要变更的上下文的负责人以及变更将如何进行至关重要。如果不了解变更的协调和优先级排序流程,这可能会成为一个巨大的绊脚石,并阻碍开发,因为团队需要等待其他人响应变更请求。显示团队关系的方向是上下文图优于传统 UML 或架构图的优势之一。在项目开始时掌握这些知识对于在非技术挑战阻碍进度之前解决它们至关重要。
Changes to business processes or the creation of new work flows can often span many bounded contexts and reach across various parts of the domain. Coordinating change on this scale often requires as much management of teams as it does technical change. It is vital to understand who is responsible for each context that is required to change and how this change will take place. If the process of coordination and prioritization of changes is not understood, it can be a massive stumbling block and stifle development as teams wait on others to act on requests for change. Showing the direction of team relationships is one of the strengths a context map has over traditional UML or architectural diagrams. Having this knowledge at the start of a project is essential to resolving nontechnical challenges before they block progress.
图 7.3展示了有界上下文之间关系的方向。对于不在同一项目的团队来说,如果需要对其职责范围之外的有界上下文进行变更,他们可能需要协调发布计划和开发优先级。技术变更可能相当简单,但如果不了解相关政策,其他上下文的变更可能会被推迟,甚至根本无法实施。
Figure 7.3 shows the direction of relationships between bounded contexts. Teams that are not on the same project might find release schedules and development priorities need to be aligned if a change is required to a bounded context outside their ownership. Technical change can be fairly straightforward, but if the political situation is not understood, changes to other contexts may be delayed or not implemented at all.
图 7.3上下文图上的组织关系。
FIGURE 7.3 The organizational relationships on a context map.
创建上下文图时,请尽量关注当前的问题区域;您需要了解会影响项目成功的环境,而不是整个企业。只关注将直接集成的上下文,有助于您顺利开展上下文图工作,并防止失去焦点。
When creating a context map, try to focus on your immediate problem area; you need to understand the landscape that will affect the success of your project and not the entire enterprise. A focus only on the contexts that you will be directly integrating with helps you get going with context mapping and prevents you from losing focus.
在规划情境并识别实际应用的模型时,最好与领域专家合作,并标记核心领域。在地图上标记核心领域,并发现其与其他情境之间的关系,可以深入了解其在企业格局中的清晰定位。
When mapping out the contexts and identifying the models in play, it is a good idea to work with your domain experts and label the core domain. Marking the core domain on the map and discovering the relationships between it and other contexts can provide insight into its clarity in context to the enterprise landscape.
大型应用程序中,上下文中的模型协同工作,提供系统行为。理解上下文之间的关系对于清晰地了解整体情况至关重要。以下模式描述了有界上下文之间的常见关系。请注意,这些模式展示了模型之间的相互关系以及团队之间的相互关系。它们并非跨上下文通信的技术集成模式。本书第二部分介绍了如何集成有界上下文的技术细节。
Models in context work together in large applications to provide system behavior. It is important to understand the relationships between the contexts to have a clear understanding as to the lay of the land. The following patterns describe common relationships between bounded contexts. Note that these patterns show how the models relate to each other and how teams relate. They are no technical integration patterns on communicating across contexts. Part II of the book covers the technicalities of how to integrate bounded contexts.
如果您正在为一个子系统创建模型,而该子系统又与其他子系统(作为大型系统的一部分)进行通信,则可能需要与不同团队创建的模型进行交互。即使是为同一领域创建的其他模型,也可能使用不同的通用语言来表达,并以与您自己的模型完全不同的方式进行建模。如果您在与这些模型集成时不够谨慎,那么适应它们的接口可能会导致模型损坏。
If you are creating a model for a sub system that communicates with other sub systems as part of a larger system you may need to interface with models created by different teams. Other models, even though created for the same domain, can be expressed with a different ubiquitous language and modelled in a completely different manner to your own. If you are not careful integrating with these models, adapting to their interfaces can lead to a corruption of your model.
为了避免损坏并保护您的模型免受外部影响,您可以创建一个隔离层,其中包含一个以您的模型编写的接口。该接口会适应并转换为其他上下文的接口。这个隔离层被称为防损坏层。
In order to avoid corruption and protect your model from external influences you can create an isolation layer that contains an interface written in terms of your model. The interface adapts and translates to the interface of the other context. This isolation layer is known as an anticorruption layer.
如图 7.4所示,您可以使用防腐层包装与遗留代码或第三方代码的通信,以保护有界上下文的完整性。防腐层管理一个上下文模型到另一个上下文模型的转换。
As shown in Figure 7.4, you can use an anticorruption layer to wrap communication with legacy or third-party code to protect the integrity of a bounded context. An anticorruption layer manages the transformation of one context’s model to another.
图 7.4使用防腐层与您不拥有或无法更改的代码集成。
FIGURE 7.4 Use an anticorruption layer to integrate with code you don’t own or can’t change.
防腐层的转换映射的工作方式与适配器模式类似,它将另一个上下文的 API 转换为您可以操作的 API。第 11 章给出了使用防腐层在有界上下文之间进行转换的示例。
The anticorruption layer’s translation map works in a similar manner to the adapter pattern in that it transforms the API of another context into an API that you can work against. Chapter 11 gives an example of the transformation that occurs between bounded contexts using the anticorruption layer.
您不会总是在进行全新开发,因此经常需要与第三方或旧版上下文集成。由于您无法更改不属于您或无法轻松更改的上下文的 API,因此务必不要为了兼容其他上下文的 API 而损害有界上下文的完整性。
You won’t always be working on greenfield developments, so you will often need to integrate with third-party or legacy contexts. Because you can’t change the API of the contexts you don’t own or those that can’t be changed easily, it’s important not to compromise the integrity of your bounded context to match the API of another.
如果您有一个类似于 BBoM 的系统,并且需要引入额外的功能,那么直接添加代码并最终导致混乱的情况会很诱人;或者,您也可以在添加新功能的同时请求重写整个系统。这两种选择都不切实际,因为重写大型应用程序既耗时又有风险,而直接添加代码则会加剧维护难度。一个更务实的选择是依靠防腐层,它可以将新上下文与现有的代码混乱隔离开来。在这种情况下使用防腐层是一种很好的重构实践,因为您可以创建清晰的边界,而无需更新上下文中混乱的代码。
If you have a system that resembles a BBoM and you need to introduce additional functionality it is tempting to simply add code to it and in turn add to the mess; alternatively you can request to rewrite the entire system at the same time as adding the new feature. Neither of these two options is practical as it can be time consuming and risky to rewrite a large application, and simply adding to the mess can increase the maintenance nightmare. A more pragmatic option is to lean on the anticorruption layer, which can be used to isolate the new context from the existing code mess. Using an anticorruption layer in this context is a great refactoring practice because you are able to create clear boundaries without needing to update the mess of code that lives within a context.
如果两个团队在同一个应用程序中紧密合作,在两个独立的有界上下文中,而这些上下文在领域概念和逻辑方面存在大量交叉,那么保持团队隔离并使用转换映射从一个上下文转换到另一个上下文的开销可能会过高。在这种情况下,协作并共享部分模型以简化集成可能是一个更好的选择。这种共享模型称为共享内核。如果同一子域中有两个有界上下文共享领域逻辑的子集,则该模式尤其适用。
If two teams are working closely in the same application, on two separate bounded contexts that have a lot of crossover in terms of domain concepts and logic, the overhead of keeping the teams isolated and using translation maps to translate from one context to another can be too much. In this instance, it may be better to collaborate and to share part of the model to ease integration. This shared model is known as a shared kernel. The pattern is of particular use if you have two bounded contexts in the same subdomain that share a subset of domain logic.
图 7.5显示了 ERP 系统的一部分,其中包含工资单上下文和共享员工模型的 HR 上下文。
Figure 7.5 shows part of an ERP system that contains a payroll context and an HR context that shares the employee model.
图 7.5与共享内核的集成。
FIGURE 7.5 Integration with a shared kernel.
由于存在共享代码依赖关系,共享内核的风险可能更大,因为耦合度更高,可能导致一个团队破坏另一个团队的系统。重要的是,两个团队的每个人都必须理解这一点,并且当通用模型被修改时,持续集成测试系统能够验证两个模型的行为。
Because there is a shared code dependency, a shared kernel can be more risky due to the tighter coupling that leads to one team being able to break another team’s system. It’s important that everyone on both teams understands this and that a continuous integrated test system verifies the behavior of both models when the common model is modified.
与您通信的其他系统或组件会使用某种类型的转换层,以便将您的模型转换为它们自己的术语,类似于防腐层。如果多个消费者共享相同的转换逻辑,那么提供一组服务会更有用,这些服务通过明确定义的显式契约(称为开放主机服务)来公开上下文的功能。
Other systems or components that communicate with you will employ some type of transformation layer in order to translate your model into terms of their own, similar to the anticorruption layer. If multiple consumers share the same transformation logic it can be more useful to provide a set of services that exposes the functionality of a context via a clearly defined, explicit contract known as an open host service.
考虑图 7.6中的示例。订单管理系统向商务系统、采购系统和 CRM 系统提供有关客户订单的信息。每个系统都需要将复杂的订单管理系统的订单模型转换为在其自身系统中使用。为了避免这种重复工作,订单管理系统可以通过开放主机服务,使用已发布的语言公开简化版本的销售订单,如图7.7所示。
Consider the example in Figure 7.6. The order management system provides information about customer orders to the commerce system, procurement system, and the CRM system. Each system is required to translate the complicated order management system’s order model for use within their own system. To avoid this duplication of efforts the order management system can expose a simplified version of a sales order using a published language via an open host service, as shown in Figure 7.7.
图 7.6多个子系统与类似的转型工作相结合。
FIGURE 7.6 Multiple subsystems integrating with similar transformation efforts.
图 7.7与开放主机服务的集成。
FIGURE 7.7 Integration with an open host service.
如果由于技术复杂性或政策因素导致不同情境之间的集成成本过高,可以决定完全不进行集成,而只是让各个团队各自独立实施。集成可以通过用户界面或手动流程实现。例如,对于管理客户联系的客户服务应用程序来说,在处理客户查询时向用户显示未完成的订单可能很有用。但是,如果订单管理系统之间的集成工作量过大,那么更实际的做法可能是简单地添加一个菜单链接,允许在单独的屏幕中打开订单管理系统,从而为用户提供所需的信息,而无需完全集成的复杂性,尽管这会略微缩小功能需求的范围。
If the cost of integration between contexts is too great due to technical complexities or political ones, a decision can be made to not integrate contexts at all and simply have teams implement separately from one another. Integration can instead be achieved via user interfaces or manual processes. For example, it may be useful for a customer service application that manages contacts with customers to also show users the orders a customer has outstanding when dealing with a query. However, if the effort of integration between an order management system is too great it may be more practical to simply include a menu link that enables the Order Management system to be opened in a separate screen, thus giving users the information they need without the complexities of fully integrating albeit for a small de-scope in feature request.
如果两个团队负责不同的上下文,但致力于实现共同目标,则可以建立合作伙伴关系,以确保在两个上下文之间进行集成合作。合作可以涵盖技术接口,以便兼顾双方的利益。从策略角度来看,两个团队可以协调发布,以便在需要时发布必要的接口和联系点。如果团队在两个限界上下文之间使用共享内核,建议他们以合作伙伴关系的方式进行。
If two teams are responsible for different contexts but are working toward a common goal a partnership can be formed to ensure that cooperation on integration between the two contexts can be made. Cooperation can cover the technical interfaces so that they accommodate both teams’ interests. From a political standpoint releases can be aligned between the teams so that necessary interfaces and contact points are released at a time that they are required. If teams are using a shared kernel between two bounded contexts it is recommended that they do so as a partnership.
有界上下文之间的关系可以用方向来定义;一端是上游,另一端是下游。如果你是关系的下游端,那么依赖于上游端的数据或行为。上游端会影响下游环境。例如,如果上游接口发生变化,下游消费端也必须随之变化。同样,上游关系的发布计划也会影响下游环境,因为它可能依赖于特定的 API 方法。以下模式展示了如何对上下游关系进行分类。
The relationships between bounded contexts can be defined in terms of a direction; one end will be upstream and the other downstream. If you are the downstream end of the relationship you are dependent on data or behavior of the upstream end. The upstream end will influence the downstream context. For instance, if an upstream interface changes so must the consuming side downstream. Likewise the release plan of the upstream part of the relationship will influence the downstream context as it may be dependent on a particular API method. The following patterns show how the upstream and downstream relationships can be classified.
在团队未能朝着共同目标努力的情况下,为了避免上游团队包揽所有决策,并可能损害下游团队的利益,从而损害整个项目,可以建立更具协作性的客户-供应商关系。在这种模式下,团队将共同努力,创建一个在技术和调度方面均令人满意的、双方都认可的接口。这种关系中的客户部分指的是下游环境。客户将参加供应商(上游环境)的计划会议,以确保其需求得到理解,并能够及时了解上游发生的变化。
In situations where teams are not working toward a common goal to avoid the upstream team making all the decisions and potentially compromising the downstream team to the detriment of the project as a whole, a more collaborative customer-supplier relationship can be formed. In this pattern, the teams work together to create an agreed-upon interface that satisfies both from a technical and scheduling standpoint. The customer part of the relationship is the downstream context. The customer will join the supplier’s (upstream context) planning meeting to ensure its needs are understood and that it can have visibility when upstream changes are occurring.
由于客户/供应商关系协作的增加,决策时间可能会更长。团队需要召开会议或在线讨论才能取得进展。通过周密的规划,或许可以提前达成一致,这样就不会有团队因为等待其他团队做出决策或部署使用更新界面的新系统而陷入困境。另一方面,对于远程团队、跨时区团队或日程繁忙的团队来说,协作开销可能会导致长时间的延迟。
Due to the increased collaboration of the customer/supplier relationship, decisions can take longer. Teams need to have meetings or online discussions to progress. With careful planning, it might be possible to make agreements in advance so that no team is blocked waiting for the other to make a decision or deploy its new system with the updated interface. On the other hand, for remote teams, teams in different time zones, or teams with busy schedules, the collaboration overhead could cause lengthy delays.
客户-供应商关系强调客户团队的有界上下文依赖于供应商团队的有界上下文,但反之则不然。有时下游上下文没有机会与上游上下文建立合作关系,因此下游上下文必须遵循上游上下文的集成点。
The customer-supplier relationship emphasizes that the customer team’s bounded context relies on the supplier team’s bounded context, but not vice versa. Sometimes there is no opportunity to form a collaborative relationship with an upstream context and so the downstream context must conform to the upstream context’s integration points.
例如,如图 7.8所示,商务环境需要的销售订单信息比订单管理环境目前提供的要多。负责商务环境的团队可以在订单管理环境团队的规划会议期间充当客户的角色,以确保他们的需求得到理解和满足。
For example, consider Figure 7.8; the commerce context requires more information on a sales order than is currently supplied from the order management context. The team that is responsible for the commerce context can act as a customer during the order management context team’s planning sessions to ensure their needs are understood and accounted for.
图 7.8有界上下文之间的客户-供应商关系。
FIGURE 7.8 A customer-supplier relationship between bounded contexts.
如果上游环境无法协作,则下游环境在集成时需要遵循上游环境。最常见的顺从关系是与外部供应商集成。几乎可以肯定,支付提供商不会为您更改其 API 并向您提供额外信息,除非您是具有影响力的客户,这会使您处于他们的上游。相反,如果您处于下游并且无法建立客户-供应商关系,并且创建防腐层的成本过高,则应该遵循提供商的模型以简化集成。顺从关系最明显的缺点是,按照上游团队的要求工作的下游团队可能不得不牺牲其领域模型的清晰度,因为它必须与上游环境的模型保持一致,即使它在概念上可能与您自己的观点不同。或者,可以使用防腐层来保持完整性,以便对接触点的更改不会影响底层模型。
If an upstream context is not able to collaborate then the downstream context will need to conform to the upstream context when integrating. The most common occurrence of the conformist relationship is integrating with external suppliers. It’s almost certain that a payment provider will not change its API for you and give you extra information unless you are an influential client, which would make you upstream of them. Instead, if you are downstream and are unable to form a customer-supplier relationship and it is too costly to create an anticorruption layer you should conform to the model of the provider to simplify integration. The most obvious downside to the conformist relationship is that the downstream team, which works to the requirements of the upstream team, may have to sacrifice clarity of its domain model because it must align to the model of the upstream context even though it may be conceptually different than your own view. Alternatively, an anticorruption layer can be used to retain the integrity so that changes to a contact point don’t affect the underlying model.
在绘制上下文图时,您可以在连接两个有界上下文的线上添加现有的组织关系类型以及它们之间的技术集成类型。如果适用,您还可以使用字母或符号来指示线的哪一侧是上游,哪一侧是下游。图 7.9展示了一个包含这些特征的上下文图示例。
When drawing up your context maps, you can add the type of organizational relationship that exists as well as the type of technical integration between two bounded contexts on the line that joins them. You can also indicate which side of the line is upstream and which is downstream using letters or symbols, if applicable. Figure 7.9 shows an example of a context map with these features.
图 7.9上下文图显示了有界上下文之间的集成类型。
FIGURE 7.9 A context map showing the types of integration between bounded contexts.
从很多方面来看,对于刚开始项目的团队来说,技术和组织层面的有界上下文之间的沟通比有界上下文本身更为重要。上下文图提供的信息可以帮助团队做出重要的战略决策,从而提高项目的成功率。上下文图是一个强大的工具,它可以让新团队成员快速上手,并对潜在的问题热点提供预警。上下文图还可以揭示企业内部沟通和工作流程中的问题。
In many ways, the communication between bounded contexts, both technical and organizational, is more important for teams starting out on a project than the bounded contexts themselves. Information that context maps provide can enable teams to make important strategic decisions that improve the success of a project. A context map is a powerful artifact that can bring new team members up to speed quickly and provide an early warning for potential trouble hot spots. Context maps can also reveal issues with communication and work flows within the business.
组织中的所有开发团队都需要了解上下文映射。团队无需了解每个有界上下文的内部工作原理;相反,他们需要了解其他上下文——它们公开的应用程序编程接口 (API)、它们之间的关系,以及最重要的,它们负责的概念模型。有了这些信息,团队可以避免责任界限模糊,并确保所有上下文都保持完整性。
All development teams in the organization need to understand the context map. Teams don’t need to understand the inner workings of each bounded context; instead, they need to be aware of those other contexts—the application programming interface (API) they expose, the relationships they have, and, most importantly, the conceptual models they are responsible for. With this information, teams can prevent blurring the lines of responsibility and ensure that all contexts retain their integrity.
保持完整性对于确保代码库专注于单一模型至关重要。这使得代码变得灵活,因为任何更改都只会影响单个有界上下文,而不会对业务领域的多个部分产生连锁反应。正是这种灵活性,让您能够修改代码,并在业务需要更改流程或逻辑时快速、自信地做出调整。
Retaining integrity is important to keep your codebase focused on a single model. This enables the code to become supple because any change affects only a single bounded context and doesn’t have a rippling effect across multiple areas of your domain. It’s this suppleness that enables you to alter code and to adapt quickly and confidently when the business needs a change to process or logic.
上下文图突出显示了混乱和混乱的区域,更重要的是,它突出了核心领域的位置。团队可以使用这些信息来确定他们需要首先清理的区域,以提高项目的成功率:
A context map highlights areas of confusion and mess, and, more importantly, where the core domain is. Teams can use this information to identify areas they need to clear up first to improve the success of a project:
问责制和责任制是可能影响项目的其他非技术领域。明确需要集成的子系统的团队所有权和管理权,对于确保变更按时完成并符合预期至关重要。上下文映射涉及调查和澄清;您可能无法立即绘制清晰的上下文映射,但明确责任、明确界定模糊界限以及在映射上下文时理解沟通流程的过程与最终的成果同等重要。
Accountability and responsibility are other nontechnical areas that can affect a project. Defining team ownership and management for subsystems that you need to integrate with is essential for ensuring changes are made on time and in line with what you expect. Context mapping is about investigation and clarification; you may not be able to draw a clear context map straight away, but the process of clarifying responsibility, explicitly defining blurred lines, and understanding communication flow while mapping contexts is as important as the finished artifact.
在有界上下文之间发生并利用有界上下文的业务流程,往往处于无人区,缺乏明确的职责,也缺乏清晰的边界和集成方法。上下文图侧重于关系的非技术层面,可以揭示系统和业务功能之间中断的业务流程和通信,这些流程和通信随着时间的推移而退化。这种揭示通常对能够更好地理解和改进跨部门和跨功能流程的企业更有用。这种洞察可以用来降低项目失败的风险,方法是及早解决模糊性问题,并提出有助于项目成功的有力问题。
The business processes that happen between and take advantage of bounded contexts are often left in no-man’s-land without clear responsibility and clarity regarding their boundaries and integration methods. A context map, focusing on the nontechnical aspects of the relationships, can reveal broken business process flow and communication between systems and business capabilities that have degraded over time. This revelation is often more useful to the businesses that are able to better understand and improve process that spans across departments and capabilities. The insight can be used to reduce risk of project failure by tackling ambiguity early and asking powerful questions that help the success of the project.
管理业务流程的环境之间的灰色区域在发生变更时也没有责任,并且只会在项目生命周期的后期才被发现。
The often gray area between contexts that govern business process is also void of accountability when changes are being made, and is only discovered later on in a project’s life cycle.
上下文图揭示了项目涉及的部门边界。如果您的团队并非掌握所有相关上下文,则需要与其他团队和其他管理层进行协调并确定优先级。提前了解这些障碍可以大大提高项目成功的可能性,并使您能够在发布计划等非技术问题成为阻碍之前将其解决。
Context maps reveal the departmental boundaries involved in a project. If your team does not own all the contexts in play, coordination with other teams and other lines of management and prioritization needs to take place. Understanding these obstacles up front gives you a much greater probability of success on a project and enables you to tackle nontechnical problems such as release scheduling before they become blockers.
类似地,需要与第三方环境集成的更改可能会暴露对测试环境和与外部团队协调的要求,或者至少需要访问沙盒帐户和文档。
In a similar manner, changes that require integration with third-party contexts can expose requirements on testing environments and coordination with outside teams or at least access to sandbox accounts and documentation.
当图表显示你的有界上下文与另一个有界上下文之间存在关联时,你应该很清楚地知道需要与负责该上下文的团队进行沟通。当图表还显示你是上游团队时,你就会明白你的职责通常是领导决策,因此,你可能经常需要主动沟通。
When a diagram shows you that a relationship exists between your bounded context and another bounded context, you should have a pretty good idea that you need to be communicating with the team responsible for it. When the diagram also indicates that you are the upstream team, you understand that your responsibility is usually to lead decision making, and accordingly, you may often need to initiate communication.
您是否曾经在一家新公司开始工作,却不明白自己的系统如何融入整个系统?您是否曾经因为领域专家向您提出的问题涉及到您几乎不了解的系统部分而感到不安?拥有一个简洁而信息丰富的上下文图,供整个团队定期查看并保持更新,是确保所有团队成员了解全局的绝佳方法,或者至少让他们了解自己对系统哪些部分了解不够。如果一位领域专家向您提出您不熟悉的问题,您可以参考上下文图,了解最适合咨询的对象。
Have you ever started working for a new company and not understood how your system fits into the system as a whole? Have you ever felt uneasy about answering questions from domain experts because they came to you with problems that also touched on parts of the system you barely knew existed? Having a concise, yet informative, context map that the whole team regularly views and keeps up to date is a fantastic way to ensure all team members understand the bigger picture—or at least have an idea of which parts of the system they don’t know enough about. If a domain expert approaches you with a problem that you’re unfamiliar with, you can turn to the context map for suggestions about who it would be best to talk to.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 8 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 8 download and individually named according to the names throughout the chapter.
领域驱动设计 (DDD) 专注于通过将业务复杂性与技术问题隔离开来,从而应对构建具有复杂领域逻辑的应用程序所带来的挑战。到目前为止,本书仅探讨了使团队能够对问题领域进行实用概念抽象建模的技术。然而,本章将探讨一些模式,这些模式使领域模型能够在应用程序上下文中使用,并兼顾持久性、呈现和其他技术需求。
Domain-Driven Design (DDD) focuses on managing the challenges of building applications with complex domain logic by isolating the business complexities from the technical concerns. Up until now, this book has only looked at techniques to enable teams to model a useful conceptual abstraction of the problem domain. This chapter, however, looks at patterns that enable the domain model to be utilized in the context of an application, taking into consideration persistence, presentation, and other technical requirements.
遵循 DDD 原则开发软件并不需要使用任何特定的应用程序架构风格。但你的架构必须支持领域逻辑的隔离。
Developing software while following the principles of DDD does not require you to use any particular application architecture style. But one thing that your architecture must support is the isolation of your domain logic.
为了避免代码库变成“大泥球”(BBoM),从而削弱领域模型的完整性和最终实用性,应用程序的结构必须支持将技术复杂性与领域复杂性分离。应用程序的呈现、持久化和领域逻辑关注点会以不同的速率和原因发生变化;分离这些关注点的架构可以适应变化,而不会对代码库中不相关的部分造成不良影响。
To avoid turning your codebase into a Big Ball of Mud (BBoM) and thus weakening the integrity and ultimately the usefulness of a domain model, it is vital that the structure of an application supports the separation of technical complexities from the complexities of the domain. Presentation, persistence, and domain logic concerns of an application will change at different rates and for different reasons; an architecture that separates these concerns can accommodate change without causing an undesired effect to unrelated areas of the codebase.
除了关注点分离之外,应用程序架构还必须通过公开一组粗粒度的用例来抽象化复杂领域的复杂性,这些用例封装并隐藏了底层领域细节。更高层次的抽象可以防止领域逻辑的更改影响表示层,反之亦然,因为应用程序的客户端通过充当用例的应用服务进行通信,而不是直接与领域对象进行通信。
In addition to a separation of concerns, an application architecture must abstract away from the intricacies of a complex domain by exposing a coarse-grained set of use cases that encapsulate and hide the low-level domain details. Abstracting at a higher level prevents changes in domain logic from affecting the presentation layer and vice versa because the clients of the application communicate through application services acting as use cases rather than directly with domain objects.
为了支持关注点分离,您可以对应用程序的不同职责进行分层,如图 8.1所示。Fowler 在其著作《企业应用程序架构模式》中对无处不在的分层架构进行了分类。然而,许多其他架构也通过将应用程序划分为共同变化的区域来支持关注点分离,例如 Bob 大叔的清洁架构、六边形架构(也称为端口和适配器架构)以及洋葱架构。
To support the separation of concerns, you can layer the different responsibilities of an application, as shown in Figure 8.1. Fowler catalogued the ubiquitous layered architecture in his book Patterns of Enterprise Application Architecture. However, many other architectures support the separation of concerns by dividing an application into areas that change together, such as Uncle Bob’s Clean Architecture, the Hexagonal Architecture (also known as the Ports and Adapters Architecture), and the Onion Architecture.
图 8.1分层架构。
FIGURE 8.1 A layered architecture.
与典型的分层架构视图不同,图 8.1显示,该架构的核心是领域层,它包含与业务相关的所有逻辑。领域层周围是一个应用层,它抽象了领域的底层细节,并将其置于粗粒度的应用程序编程接口 (API) 之后,这些 API 代表了应用程序的业务用例。领域逻辑层和应用层相互隔离,并免受任何客户端、框架和基础设施问题带来的意外复杂性的影响。
Unlike typical views of a layered architecture, Figure 8.1 shows that at the heart of the architecture is the domain layer containing all the logic pertaining to the business. Surrounding the domain layer is an application layer that abstracts the low-level details of the domain behind a coarse-grained application programming interface (API) representing the business use cases of the application. The domain logic and application layers are isolated and protected from the accidental complexities of any clients, frameworks, and infrastructural concerns.
为了强制实现关注点分离,架构中心的领域层和应用层不应依赖于任何其他层。所有依赖关系都面向内部,因此,作为应用程序核心的领域层不依赖于任何其他层,使其能够专注于领域关注点,不受任何干扰。应用层仅依赖于领域层;它通过委托给领域层来协调用例的处理。
To enforce a separation of concerns, the domain layer and application layers at the center of the architecture should not depend on any other layers. All dependencies face inward, in that the domain layer, at the heart of the application, is dependent on nothing else, enabling it to focus distraction-free on domain concerns. The application layer is dependent only on the domain layer; it orchestrates the handling of the use cases by delegating to the domain layer.
当然,领域对象的状态需要保存到某种持久化存储中。为了在不将领域层与技术代码耦合的情况下实现这一点,应用层定义了一个接口,该接口允许领域对象进行数据融合和持久化。该接口从应用层的角度编写,采用应用层能够理解的语言和风格,不受特定框架或技术术语的束缚。然后,基础架构层实现并适配这些接口,从而在不耦合的情况下提供下层所需的依赖关系。事务管理以及安全性和日志记录等横切关注点也以相同的方式提供。图 8.2展示了依赖关系的方向和接口的方向,它们描述了应用层和技术层之间的关系。
Of course, the state of domain objects needs to be saved to some kind of persistence store. To achieve this without coupling the domain layer to technical code, the application layer defines an interface that enables domain objects to be hydrated and persisted. This interface is written from the perspective of the application layer and in a language and style it understands free from specific frameworks or technical jargon. The infrastructural layers then implement and adapt to these interfaces, thus giving the dependency that the lower layers need without coupling. Transaction management along with cross-cutting concerns such as security and logging are provided in the same manner. Figure 8.2 shows the direction of dependencies and the direction of interfaces that describe the relationship between the application layer and technical layers.
图 8.2分层架构内的依赖倒置。
FIGURE 8.2 Dependency inversion within a layered architecture.
如第四章所述,领域模型代表了问题域的概念抽象视图,旨在满足业务用例的需求。包含抽象模型的领域层不依赖于任何其他内容,并且与它所服务的客户端以及用于持久化领域对象的数据存储的技术细节无关。
As discussed in Chapter 4 a domain model represents a conceptual abstract view of the problem domain created to fulfill the needs of the business use cases. The domain layer containing the abstract model does not depend on anything else and is agnostic to the technicalities of the clients it serves and data stores that persist the domain objects.
应用服务层代表应用程序的用例和行为。用例被实现为包含应用逻辑的应用服务,通过委托给领域层和基础架构层来协调用例的执行。应用服务在比领域对象更高的抽象级别上运行,公开一组粗粒度的服务,同时隐藏领域层的细节——系统做什么,但不知道如何做。通过将领域的复杂性隐藏在外观层之后,您可以改进领域模型,同时确保更改不会影响客户端。
The application service layer represents the use cases and behavior of the application. Use cases are implemented as application services that contain application logic to coordinate the fulfillment of a use case by delegating to the domain and infrastructural layers. Application services operate at a higher level of abstraction than the domain objects, exposing a coarse-grained set of services while hiding the details of the domain layer—what the system does, but not how it does it. By hiding the complexities of the domain behind a façade, you can evolve the domain model while ensuring that changes do not affect clients.
领域层的客户端是应用服务层;然而,为了执行其工作,它需要依赖外部层。这些依赖关系是反向的,因为应用层将契约暴露给它所需的接口。外部资源必须适应这些接口,以确保应用层不会与特定技术紧密耦合。
The client of the domain layer is the application service layer; however, to perform its work, it requires dependencies on external layers. These dependencies are inverted because the application layer exposes the contracts to the interfaces it requires. The external resources must then adapt to the interfaces to ensure the application layer is not tightly coupled to a specific technology.
应用服务层负责协调从数据存储中检索领域对象、将工作委托给这些对象,然后保存更新后的状态。应用服务层还负责在领域内发生重大事件时协调向其他系统的通知。所有这些与外部资源的接口都在应用服务层中定义,但在基础架构层中实现。
Coordinating the retrieval of domain objects from a data store, delegating work to them, and then saving the updated state is the responsibility of the application service layer. Application service layers are also responsible for coordinating notifications to other systems when significant events occur within the domain. All these interfaces with external resources are defined within the application service layer but are implemented in the infrastructural layer.
应用服务层能够在不损害领域层完整性的情况下支持不同的客户端。新客户端必须适应应用程序契约(即 API)定义的输入。它们还必须将应用服务的输出转换为适合自己的格式。这样,应用层就可以被视为一个防腐层,确保领域层保持纯粹,不受外部技术细节的影响。
The application service layer enables the support of disparate clients without compromising the domain layer’s integrity. New clients must adapt to the input defined by the application’s contract—its API. They must also transform the output of the application service into a format that is suitable for them. In this way, the application layer can be thought of as an anticorruption layer, ensuring that the domain layer stays pure and unaffected by external technical details.
应用程序的基础架构层是使其能够正常运行的技术细节。应用层和领域层分别专注于建模行为和业务逻辑,而基础架构层则关注纯粹的技术功能,例如使应用程序能够被用户使用,无论是由人类通过用户界面,还是由应用程序通过一组 Web 服务或消息端点访问。基础架构层还负责存储领域对象状态信息的技术实现。
The infrastructural layers of an application are the technical details that enable it to function. Whereas the application and domain layers are focused on modeling behavior and business logic, respectively, the infrastructural layers are concerned with purely technical capabilities, such as enabling the application to be consumed, whether by humans via a user interface or by applications via a set of web service or message endpoints. The infrastructural layers are also responsible for the technical implementation of storing information on the state of domain objects.
此外,基础架构层还可以提供日志记录、安全、通知以及与其他有界上下文和应用程序集成的功能。这些都是外部细节——技术问题,不应直接影响暴露的用例和应用程序的领域逻辑。
In addition, the infrastructural layer can provide capabilities for logging, security, notification, and integration with other bounded contexts and applications. These are all external details—technical concerns that should not directly affect the use case exposed and the domain logic of an application.
跨层通信时,为了防止将领域模型的细节暴露给外界,请勿跨层传递领域对象。出于同样的原因,请勿将未经请求的原始数据或用户输入直接发送到领域层。相反,您可以使用简单的数据传输对象 (DTO)、演示模型和应用程序事件对象来传达领域中的更改或操作。
When communicating across layers, to prevent exposing the details of the domain model to the outside world, you don’t pass domain objects across boundaries. For the same reasons, you don’t send raw unsolicited data or user input straight into the domain layer. Instead, you use simple data transfer objects (DTOs), presentation models, and application event objects to communicate changes or actions in the domain.
为了避免层与层之间紧密耦合,高层必须通过适配低层的消息类型来与其通信。这又一次保持了低层与外部层的隔离和松散耦合。图 8.3展示了跨层通信以及如何转换数据以保护领域模型的完整性。
To avoid tight coupling of layers, higher layers must communicate with lower layers by adapting to their message types. This again keeps the lower layers isolated and loosely coupled to any external layers. Figure 8.3 shows the communication across the layers and how data is transformed to protect the integrity of the domain model.
图 8.3域对象对应用程序的客户端是隐藏的。
FIGURE 8.3 Domain objects are hidden from clients of the application.
分离应用程序中的不同关注点并确保域逻辑不依赖于任何技术重点代码(例如演示或数据持久性框架),使您能够独立于任何基础架构框架来测试域和应用程序逻辑。
Separating the different concerns in your application and ensuring your domain logic is not dependent on any technically focused code such as presentation or data persistence frameworks enables you to test domain and application logic in isolation, independent of any infrastructural frameworks.
如图 8.4所示,您可以使用单元测试来确认领域层内的逻辑。您可以使用模拟和存根为应用层提供所需的虚假实现,以确认业务任务与领域层和外部资源协调的正确性。
As shown in Figure 8.4, you can use unit tests to confirm the logic within the domain layer. You can use mocks and stubs to give the application layer the fake implementations it requires to confirm the correctness of business task coordination with the domain layer and external resources.
图 8.4单独测试各层。
FIGURE 8.4 Testing layers in isolation.
除了在应用程序代码库中分离关注点之外,架构还必须将领域对象状态的持久性与其他应用程序的数据需求分离。图 8.5展示了通过共享数据库和共享模式集成的应用程序。
In addition to separating the concerns within the codebase of an application, an architecture must include the separation of the persistence of the domain object state from other applications’ data requirements. Figure 8.5 shows applications integrated via a shared database and shared schema.
图 8.5通过共享数据模式集成的有界上下文。
FIGURE 8.5 Bounded contexts integrating via a shared data schema.
虽然这是一种简单的集成方法,但它可能会使模型变得复杂,模糊其界限,因为它会成为代码库发展成 BBoM 的催化剂。共享数据使得客户端代码很容易绕过有界上下文的保护,并在没有领域逻辑保护的情况下与领域对象状态进行交互。此外,还很容易错误地解释逻辑和模式,导致状态更改,从而使不变量失效。
Although this is an easy integration method, it can complicate and blur the lines of a model by acting as the catalyst to your codebase growing into a BBoM. Sharing data makes it easy for client code to bypass the protection of a bounded context and interact with a domain object state without the protection of domain logic. It is also easy to interpret logic and schema incorrectly, resulting in changes to the state that invalidate invariants.
如图 8.6所示,您应该优先使用应用数据库或有界上下文数据库,而不是集成数据库。正如您在领域模型中应用上下文边界一样,您必须对持久化模型执行同样的操作。这有助于强制客户端通过定义明确的应用服务层进行集成,从而保护模型的完整性并确保满足不变量。
As shown in Figure 8.6, you should favor application or bounded context databases over integration databases. Just as you apply context boundaries within the domain model, you must do the same for the persistence model. This helps to force clients to integrate through the well-defined application service layer, protecting the integrity of your model and ensuring invariants are met.
图 8.6具有自己的数据模式的有界上下文。
FIGURE 8.6 Bounded contexts with their own data schema.
应用程序可以由多个有界上下文组成。架构以不同的方式应用于有界上下文和应用程序。由两个或多个有界上下文组成的应用程序可能具有针对用户界面的架构风格,并且每个有界上下文可能具有不同的架构风格。图 8.7展示了一个由三个有界上下文组成的应用程序;其中,表示层包含其自己的应用层,以便于与有界上下文进行协调。
Applications can be composed of more than one bounded context. Architectures apply to bounded contexts and applications in different ways. An application that is composed of two or more bounded contexts may have an architectural style for the user interfaces and different architectures for each of the bounded contexts. Figure 8.7 shows an application composed of three bounded contexts; here the presentation layer contains its own application layer to facilitate the coordination with the bounded contexts.
图 8.7通过单独的应用层集成的有界上下文。
FIGURE 8.7 Bounded contexts integrating via a separate application layer.
然而,有些人认为有界上下文的边界应该延伸到表示层。Udi Dahan 的业务组件赋予了有界上下文拥有用户界面特定区域的责任。该架构如图 8.8所示。
However, some people believe that the boundary of a bounded context should extend to the presentation layer. Udi Dahan’s business component gives the bounded context the responsibility for owning specific regions of the user interface. This architecture can be seen in Figure 8.8.
图 8.8由有界上下文组成的表示层。
FIGURE 8.8 Presentation layer composed of bounded contexts.
在这种架构风格中,基础设施负责确保通信和关联 ID 的共享。
In this architecture style, the infrastructure takes care of ensuring communication and the sharing of correlation IDs.
跨有界上下文的架构风格或数据存储不需要保持一致,但在单个有界上下文中,您应该努力遵循一种表示域逻辑的方法。
There does not need to be consistency in architectural styles or data stores across bounded contexts, but within a single bounded context, you should strive to follow one method of representing domain logic.
应用服务层,在 Fowler 的《企业应用程序架构模式》一书中被归类为服务层,可用于定义领域模型的边界,也可以被认为是有界上下文概念的实现,隔离和保护领域模型的完整性。
The application service layer, cataloged as the service layer in Fowler’s Patterns of Enterprise Application Architecture book, can be used to define the boundary of your domain model and can also be thought of as the implementation of the bounded context concept, isolating and protecting the integrity of your domain model.
正如本章前面提到的,应用服务层的职责是公开应用程序可用的功能和操作,同时抽象低级复杂性领域模型。功能由系统必须满足的业务用例定义。应用服务通过协调领域逻辑的执行来实现用例。它们处理诸如处理输入和调整领域状态报告信息等技术问题,以及事务、日志记录和持久化问题。
As mentioned earlier in this chapter the responsibility of the application service layer is to expose the capabilities and operations available to the application while abstracting the low-level complexities of the domain model. Capabilities are defined by the business use cases that the system must satisfy. The application services fulfill the use cases by coordinating the execution of domain logic. They deal with technical concerns such as handling input and shaping reporting information on the state of the domain, as well as transactional, logging, and persistence concerns.
应用服务仅包含应用逻辑。这些逻辑涵盖安全性、事务管理以及与其他技术功能(例如电子邮件和 Web 服务)的通信。它们是领域层的客户端,并将所有工作委托给该层。应用服务中不应包含任何领域逻辑;相反,应用服务应采用过程式且精简的架构。应用层不依赖于任何使用应用服务的框架或技术,例如 UI 或服务框架。但是,它确实定义了一些接口,用于整合领域对象并管理非领域任务。
Application services contain application logic only. This logic covers security, transaction management, and communication with other technical capabilities such as e-mail and web services. They are the clients of the domain layer and delegate all work to that layer. No domain logic should be found within the application services; instead, the application services should be procedural in style and thin. The application layer is not dependent on any frameworks or technology that consumes the application service, such as UI or service frameworks. It does, however, define interfaces that it depends on to hydrate domain objects and manage nondomain tasks.
应用逻辑包含实现业务用例所需的工作流步骤。这些步骤包括从数据库中提取领域对象、将用户输入映射到领域层能够理解的对象,以及最终委托领域对象或其集合来做出业务决策。其他步骤可能包括委托基础架构服务,例如通过消息传递系统或 Web 调用通知其他系统领域状态的变化、授权和日志记录。
Application logic contains the workflow steps required to fulfill a business use case. Steps can include the hydrating of domain objects from a database, the mapping of user input to objects that the domain layer understands, and ultimately the delegating to domain objects or a collection of them to make a business decision. Other steps may include delegating to infrastructural services, such as notifying other systems of changes in domain state via messaging systems or web calls, authorization, and logging.
应用逻辑的核心是通过委托给领域和基础架构服务来进行协调和编排。应用服务本身不执行任何工作,但它们知道应该与谁交互才能完成任务。而领域逻辑则只关注领域规则、概念、信息和工作流程。领域逻辑不受技术细节(包括持久化)的影响。
Application logic is all about coordination and orchestration through delegation to domain and infrastructural services. The application services don’t do any work, but they understand who to talk to to complete the task. Domain logic, on the other hand, is focused only on domain rules, concepts, information, and work flows. Domain logic is free from technical details, including persistence.
举例来说,请考虑图 8.9,它模拟了将促销优惠券应用于电子商务购物篮的用例。ASP.NET MVC 框架表示层将超文本传输协议 (HTTP) 请求转换为应用服务层期望的形式,并调用服务方法。应用服务委托持久层检索优惠券对象。然后,它检查优惠券是否仍然有效。如果无效,则返回相应的结果。如果有效,它再次委托持久层检索购物篮,并将购物篮传递给优惠券以生成折扣。购物篮域对象上的折扣更改将被持久化,并发布事件通知优惠券已被兑换。
As an example, consider Figure 8.9, which models the use case of applying a promotion coupon to an e-commerce basket. The ASP.NET MVC framework presentation layer transforms the Hypertext Transport Protocol (HTTP) request into a form that the application service layer expects and calls the service method. The application service delegates to the persistence layer to retrieve the coupon object. It then checks whether the coupon is still valid. If it is not, it responds with an appropriate result. If it is valid, it again delegates to the persistence layer to retrieve the basket and passes the basket to the coupon to generate a discount. The changes to the discount on the basket domain object are persisted, and an event is published to notify that the coupon was redeemed.
图 8.9应用逻辑与领域逻辑。
FIGURE 8.9 Application logic versus domain logic.
由于应用服务暴露了系统的功能,因此它们无需适应新的客户端。相反,新的客户端(例如表示层)应该适应服务暴露的契约。换句话说,系统的功能不应该为了客户端而改变。相反,它们应该只在业务用例发生变化时才进行更改。应用服务公开的用例与用于实现它们的领域逻辑相比,其变化速率和原因各不相同。这使得使用这些服务的客户端能够免受领域逻辑频繁变化的影响。
Because the application services are exposing capabilities of the system, they should not have to adapt to new clients. Instead, new clients, such as presentation layers, should adapt to the contracts exposed by the services. In other words, the capabilities of the system should not change for clients. Rather, they should change only when the business use case changes. The use cases exposed by the application services change at a different rate and for different reasons than the domain logic that is used to fulfill them. This enables clients consuming the services to be protected from frequent changes to domain logic.
以评估订单欺诈风险的业务用例为例。系统公开了获取订单详细信息并根据领域逻辑返回评分的功能。随着时间的推移,领域逻辑可能会发生变化,但作为该用例的实现(用于对订单进行风险评分)的应用服务将基本保持不变,只会在修改其合约以提供更多信息时发生变化。
Take, for example, the business use case of risk assessing an order for fraud. The system exposes the capability to take details of an order and return a score based on domain logic. Over time, the domain logic may change, but the application service that is the implementation of the use case to score an order for risk will largely remain constant, changing only to alter its contract to provide additional information.
利益相关者可能不了解领域层的复杂性;然而,应用层负责的业务任务对业务来说意义重大。即使利益相关者不是领域专家,他们也应该理解这些任务。
The stakeholders may not know the complexities of the domain layer; however, the business tasks that the application layer is responsible for are meaningful to the business. Even if they are not domain experts, the stakeholders should understand them.
领域模型本质上是面向对象的,而应用服务则是过程化的,因为它们专注于任务编排,而不是领域逻辑和概念的建模。应用服务与 ASP.NET MVC 控制器操作有些相似。控制器操作包含控制用户界面交互的逻辑,其方式与应用服务包含表示业务任务或用例的逻辑相同,这些任务或用例用于协调与领域层内的服务和对象的通信。控制器操作和应用服务本质上都是无状态的和过程化的。一个例外是,控制器操作和应用服务都可以存储状态,但该状态应该仅用于存储客户旅程的状态或业务任务的进度。
Whereas the domain model is object-oriented in its nature, the application services are procedural, as they are focused on task orchestration as opposed to modeling domain logic and concepts. Application services are somewhat similar to ASP.NET MVC controller actions. Controller actions contain logic to control the user interface interactions in the same manner that application services contain logic that represents business tasks or use cases that coordinate communication with services and objects within the domain layer. Both controller actions and application services are stateless and procedural in nature. An exception is that both controller actions and application services can store state, but the state should only be to store the status of the customer journey or the progress of the business task.
应用服务还以转换和映射输入输出的形式与控制器操作共享更多协调逻辑。控制器操作将 HTTP POST 变量映射到应用服务所需的对象,并将应用服务查询响应映射到视图模型以满足呈现需求。同样,应用服务将请求映射到领域对象能够理解的结构中,并使用隐藏领域对象真实形式且特定于用户界面视图的呈现模型进行响应。
Application services also share more coordination logic with controller actions in the form of transforming and mapping input and output. Controller actions map HTTP post variables to objects that application services require and map application services query responses to view models for presentation needs. Application services, in the same way, map requests into structures that domain objects understand, and respond with presentation models that hide the real form of the domain objects and that are specific to user interface views.
行为驱动设计 (BDD) 可帮助您理解应用程序的行为。通过 BDD 捕获的行为,您可以使用 BDD 规范中表达的语言作为应用服务用例的名称。这类似于在领域层代码中使用领域通用语言 (UL) 的方式。应用服务不仅仅是创建、读取、更新和删除 (CRUD) 方法;它们应该揭示用户意图并传达系统的功能。第 25 章“命令:用于处理业务用例的应用服务模式”中提供了这方面的示例,以及如何实现应用服务层的模式。
Behavior-Driven Design (BDD) helps you understand the behaviors of an application. With the behaviors you capture using BDD, you can use the language expressed in the BDD specifications as the name for you application services use/cases. This is similar to the way you use the ubiquitous language (UL) of the domain within the code of the domain layer. Application services are not simply create, read, update, and delete (CRUD) methods; they should reveal user intent and communicate the capabilities of a system. Examples of this can be seen in Chapter 25, “Commands: Application Service Patterns for Processing Business Use Cases,” along with patterns on how to implement the application service layer.
应用服务功能强大,可以解决任何应用程序的复杂性,无论是具有丰富逻辑的核心子域,还是仅仅是访问数据的外观的通用子域存储。通过应用服务将客户端与逻辑分离,可以使域层干净地发展,而不会在各层之间产生连锁反应。
Application services are powerful and can be helpful for any application complexity, be it a core subdomain with rich logic or a generic subdomain that is merely a façade for access to the data store. Having the application services decouple clients from the logic enables the domain layer to evolve cleanly without having a ripple effect across layers.
您的应用服务方法可以揭示是否需要领域模型。如果您发现所有业务用例都只是简单地更新、添加或删除数据,那么很可能该领域缺乏任何真正的逻辑,可以通过使用事务脚本或数据包装器模式(如第二章“提炼问题领域”)来保持简单,而不是使用成熟的富领域模型。但是,如果您系统的应用服务和行为语言丰富,这可能表明您的领域逻辑层需要领域模型模式。
Your application service methods can reveal whether a domain model is required at all. If you find that all your business use cases are simply updating, adding, or deleting data, then it’s a good bet that the domain is lacking any real logic and can be kept simple by employing a transaction script or data wrapper pattern, as discussed in Chapter 2, “Distilling the Problem Domain,” instead of a full-blown rich domain model. However, if the application services and behaviors of your system are rich in language, this may suggest the need for a domain model pattern in your domain logic layer.
除了协调业务任务之外,应用服务层还需要以报告的形式提供领域对象的状态信息。您不希望将领域模型的内部工作原理暴露给外界,因此应用服务会将领域对象转换为呈现模型,以便在不暴露领域模型结构的情况下提供领域状态的具体视图。您可以在图 8.10中看到这种转换。
Besides coordinating business tasks, the application service layer needs to provide information on the state of domain objects in the form of reports. You don’t want to expose the inner workings of your domain model to the outside world, so the application services transform domain objects into presentation models that give specific views of domain state without revealing the structure of the domain model. You can see this transformation in Figure 8.10.
图 8.10视图模型映射到许多域对象。
FIGURE 8.10 A view model mapping to many domain objects.
有时,用户界面需要跨多个领域对象的信息。如果应用服务将所有丰富的领域对象聚合起来,仅仅为视图提供一部分信息,效率低下且成本高昂。在这种情况下,应用服务层最好直接从数据源提供领域状态的特定视图,如图 8.11所示。这样,您就可以高效地构建视图,而无需构建领域对象的大型对象图并公开其中的详细信息。
Sometimes a user interface requires information that spans across many domain objects. It would be inefficient and costly for the application service to hydrate all the rich domain objects to simply provide a subset of information for a view. In these cases, it is preferable for the application service layer to provide a specific view of domain state directly from the data source, as shown in Figure 8.11. This way, you can construct views in an efficient manner without having to construct large object graphs of the domain objects and expose details within them.
图 8.11直接从数据源查询的视图模型。
FIGURE 8.11 View models queried directly from the data source.
然而,从同一概念模型(尽管是数据模型)提供读写功能存在一个缺点。事务模型将逻辑存储在域对象中,将简单状态存储在数据存储中。为了同时支持报表和事务需求,视图可能需要额外的信息,这会影响域对象的结构。为了防止模型因演示需求而发生更改,您可以将视图数据单独存储在最适合查询的数据模式中。为此,您可以存储领域模型中发生的变化并将其用作报告需求的基础。
There is, however, a drawback to providing read and write capabilities from the same conceptual model, albeit the data model. The transactional model stores logic in domain objects and simple state in the data store. To support both reporting and transactional needs, the views might require extra information that will affect the structure of domain objects. To prevent the model from having to change because of presentation needs, you can store the view data separately in a data schema that is best suited to querying. To achieve this, you can store changes that occur within the domain model and use these as the basis for reporting requirements.
图 8.12展示了事务模型如何处理来自客户端的写入请求,然后引发存储以供查询的事件。您可以将这些事件及其数据存储在同一个数据库中,也可以存储在完全不同的存储机制中。这种模式称为命令查询职责分离 (CQRS),第 24 章“CQRS:有界上下文的架构”将更详细地介绍。第 26 章“查询:领域报告”将介绍更多用于领域模型报告的模式。
Figure 8.12 shows how the transactional model handles a write request from a client and then raises events that are stored for querying. You can store these events and their data in the same database or a completely different storage mechanism. This pattern is called Command Query Responsibility Segregation (CQRS) and is covered in greater detail in Chapter 24, “CQRS: An Architecture of a Bounded Context.” Further patterns for reporting on a domain model are presented in Chapter 26, “Queries: Domain Reporting.”
图 8.12视图存储与事务存储分离。
FIGURE 8.12 View store separated from transactional storage.
应用服务层客户端的作用是公开系统的功能。许多应用程序都具有某种形式的呈现或用户界面,以便用户访问系统行为。其他应用程序则通过 RESTful 或 Web 服务公开其功能。无论客户端应用程序的类型如何,服务都应该了解其功能的使用对象。应用服务不应为了满足客户端的需求而妥协,而应公开应用程序的用例,并强制客户端适应其 API。
The role of the clients of the application service layer is to expose the capabilities of the system. Many applications have some form of presentation or user interface that will give users access to the system behaviors. Other applications instead expose their functionality via RESTful or web services. Regardless of the type of client application, a service should be ignorant to what consumes its functionality. Application services should not bend to meet the needs of a client but instead should expose use cases of an application and force a client to adapt to its API.
完全可以构建一个没有应用服务层的系统,依靠客户端执行应用服务层负责的所有任务。但是,通过创建一组特定的服务,您可以明确地建模用例,并将它们与演示要求。这些应用服务有助于关注系统的行为,并使您能够将域逻辑与应用程序的其他关注点分开。
It is entirely possible to build a system without an application service layer, relying on the clients to perform all the tasks that the application service layer is responsible for. However, by creating a specific set of services, you are modeling use cases explicitly and keeping them separate from presentation requirements. These application services help to focus on the behaviors of the systems and enable you to separate domain logic from the other concerns of your application.
图 8.13展示了多个客户端如何通过应用服务层使用应用程序的行为。同时还展示了应用服务层本身如何使用外部上下文,以及第三方服务。
Figure 8.13 shows how multiple clients can consume the behaviors of an application via the application service layer. Also shown is how the application service layer can itself consume external contexts and third party services.
图 8.13:应用程序的各种客户端。
FIGURE 8.13 :Various clients of an application.
有界上下文可以通过技术基础设施进行通信,从而形成大型系统。图 8.14展示了各种客户端如何协同工作,定义一个更大的系统。本书第二部分介绍了集成有界上下文的方法,第四部分将介绍用户界面需求。
Bounded contexts can form large systems by communicating via technical infrastructure. Figure 8.14 shows various clients working together to define a larger system. The methods of integrating bounded contexts feature in Part II of this book, with the user interface needs being covered in Part IV.
图 8.14由多个有界上下文组成的系统。
FIGURE 8.14 A system composed of multiple bounded contexts.
有时业务流程会跨越多个有界上下文。在这种情况下,您可以使用流程管理器来协调业务任务。图 8.15展示了一个包含业务任务逻辑的流程管理器,用于协调更大的流程。与应用服务类似,除了用于跟踪任务进度的状态外,流程管理器是无状态的,并且会将任何工作委托给应用程序执行。第 25 章“命令:用于处理业务用例的应用服务模式”将探讨此模式。
Sometimes business processes span multiple bounded contexts. For these cases, you employ the use of a process manager to coordinate business tasks. Figure 8.15 shows a process manager that contains business task logic to coordinate larger processes. Similar to the application services, the process managers will be stateless apart from the state used to track task progression, and will delegate back to applications to carry out any work. This pattern is explored in Chapter 25, “Commands: Application Service Patterns for Processing Business Use Cases.”
图 8.15进程管理器。
FIGURE 8.15 A process manager.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
领域驱动设计 (DDD) 是一种理念,其诞生源于重新调整开发团队在复杂领域编写软件时的重心。它并非一个框架或一组预定义的模板,可以直接应用于项目。虽然设计模式和框架本身就很有价值,但 DDD 更强调业务团队和开发团队之间协作、探索和学习的价值,从而打造出实用且相关的软件。团队应该拥抱他们所处的问题领域,并跳出技术狭隘的思维模式。成为一名优秀软件工匠的最基本要点是了解您工作中的问题领域,并像重视您的技术专长一样重视它。
Domain-Driven Design (DDD) is a philosophy born out of a need to realign the focus of development teams writing software for complex domains. It is not a framework or a set of predefined templates that you can apply to a project. Although there is value in design patterns and frameworks, DDD emphasizes the value in collaboration, exploration, and learning between the business and development team to produce useful and relevant software. Teams should embrace the problem domain they are working within and look outside of their technical tunnel vision. The most fundamental point to being a great software craftsman is to understand the problem domain you work within and value this as much as you value your technical expertise.
刚接触 DDD 或不了解核心概念的团队在将 DDD 理念应用于项目时可能会遇到问题。本文将介绍这些常见问题,并解释为什么团队难以采用该理念。
Teams new to DDD or those that do not understand the core concepts can experience problems when applying the philosophy to their projects. These common problems are presented here with explanations as to why teams are finding it difficult to adopt the philosophy.
DDD 提供了一系列战术模式,用于辅助模型驱动设计 (MDD) 和领域模型的创建。在 Google 上快速搜索一下 DDD,你就会发现这些构建块被过分强调,并且经常被误认为是 DDD 中最重要的部分。Evans 本人经常说,他希望把 DDD 中更具战略性的模式而不是构建块模式放在本书的开头,因为大多数人读完这组模式后似乎就停止阅读了。
DDD presents a selection of tactical patterns to help with Model-Driven Design and to aid in the creation of a domain model. A quick Google search on DDD shows you that these building blocks have been overemphasized and are often mistakenly thought of as the most important part of DDD. Evans himself often remarks that he wished he had put the more strategic patterns of DDD rather than the building block patterns at the beginning of the book because most people seem to stop reading after this set of patterns.
关注 DDD 的战术编码模式凸显了一个更大的问题:技术人员只专注于技术模式和编写代码。在为具有复杂逻辑的系统设计软件时,编写代码永远不会成为瓶颈。代码是开发人员和领域专家共同协作并对问题领域进行建模的成果。代码代表着协作和探索过程的终点。开发人员的工作是解决问题,而与领域专家协作,解决问题有时更容易脱离键盘。最终,可用的代码是学习和理解领域的最终成果。
A focus on the tactical coding patterns of DDD highlights a bigger problem: technical people who are only focused on technical patterns and writing code. When designing software for systems with complex logic, typing code will never become a bottleneck. The code is an artifact of developers and domain experts working together and modeling the problem domain. The code represents the end of the process of collaboration and discovery. A developer’s job is to problem solve, and problem solving is sometimes easier done away from the keyboard in collaboration with domain experts. In the end, working code is ultimately the result of learning and understanding the domain.
DDD 并不强制要求框架、工具集或应用程序架构。您无需使用 CQRS、事件溯源、事件驱动、RESTful 服务、消息传递或对象关系映射器即可应用 DDD 原则。然而,它坚持的一点是,领域模型的复杂性与技术代码的复杂性必须相互独立。任何支持这一点的架构都非常适合 DDD。领域逻辑和技术复杂性的变化速度不同;因此,组织这些不同的上下文是管理复杂性的关键。
DDD does not dictate a framework, tool set, or application architecture. You don’t have to use CQRS, event sourcing, event-driven, RESTful services, messaging, or object-relational mappers to apply the principles of DDD. What it does insist on, though, is that the complexity of your domain model is kept isolated from the complexities of your technical code. Any architecture that supports this is a good fit for DDD. Domain logic and technical complexity change at different rates; as a result, the organization of these different contexts is key to managing complexity.
架构是有界上下文的,而非特定于应用程序的。对于简单且低复杂度的有界上下文,架构师可以采用分层设计,并在领域层使用事务脚本模式。对于协作性更强的领域,可以使用 CQRS,而对于复杂的领域,则更倾向于使用富领域模型模式。
Architectures are bounded context and not application specific. The architect for a simple bounded context with low complexity could be composed of a combination of a layered design with the transaction script pattern for the domain layer. A more collaborative domain could employ CQRS, and a complex domain would favor the rich domain model pattern.
只关心编写代码的团队会专注于 DDD 的战术模式。他们把构建块模式视为圣经而非指南,却不了解何时可以打破规则。他们浪费精力去遵守模式规则。这些精力最好用来理解为什么需要编写这些规则。DDD 的重点在于发现需要编写什么、为什么需要编写以及应该投入多少精力。如前所述,DDD 的战术模式是自 Eric 的书出版以来发展最快的元素,而 DDD 的战略方面则始终忠实于 Eric Evan 的原著。如何开发团队创建领域模型远不如理解首先要编写什么模型以及如何在有界上下文中开发它们那么重要。理解解决问题的内容和原因比如何在代码中实现它更重要。
Teams concerned only with writing code focus on the tactical patterns of DDD. They treat the building block patterns as a bible rather than a guide, with no understanding of when it’s okay to break the rules. They spend wasted effort adhering to the rules of the patterns. This energy is better spent on understanding why it needs to be written in the first place. DDD is about discovering what you need to write, why you need to write it, and how much effort you should use. As mentioned before, the tactical patterns of DDD are the elements that have evolved the most since Eric’s book was written, with the strategic side of DDD remaining faithful to Eric Evan’s original text. How development teams create domain models is not nearly as important as understanding what models to write in the first place and how to develop them in a bounded context. Understanding the what and the why of problem solving is a more important process than how you are going to implement it in code.
许多 DDD 项目失败的原因在于,他们选择了 DDD 的战术模式,却忽略了其战略和协作层面。团队没有花时间与业务部门进行知识交流。他们没有专注于领域模型和细致的抽象。他们没有建立通用语言 (UL)。仅使用 DDD 的战术模式语言被称为 DDD 精简版。遵循 DDD 精简版方法固然可行,但这并非秉持 DDD 哲学。误以为自己正在应用 DDD 的团队将会错过 DDD 的诸多价值所在:协作、UL 和有界上下文。仅仅关注辅助建模设计的模式,会忽略解决问题的全局视角。
Many DDD projects fail because the tactical patterns of DDD are picked, but the strategic and collaborative sides of DDD are neglected. Teams do not take the time to knowledge-crunch with the business. They do not concentrate on the domain model and on careful abstractions. They don’t establish a ubiquitous language (UL). Using only the tactical pattern language of DDD is known as DDD lite. Following a DDD lite approach is fine, but this is not embracing the DDD philosophy. Teams mistakenly thinking that they are applying DDD will be missing out on much of where the value of DDD lies: collaboration, UL, and bounded contexts. Focusing only on the patterns to aid the modeling design omits the bigger picture of problem solving.
相比之下,DDD 的许多战略模式可用于创建任何中大型业务系统,无论其底层复杂度如何。事实上,所有战略模式都有很多好处,包括确定是否应该定义 UL 以及是否应该使用战术模式。子域可以帮助分解复杂的问题域,从而促进沟通并确定重要内容。上下文图揭示了不同上下文之间的集成点以及团队之间的关系。然而,有时很难证明将领域模型模式应用于复杂或不断发展的领域以外的任何领域是合理的。
In contrast, many of the strategic patterns of DDD can be used in the creation of any medium-to-large-scale business system regardless of the underlying complexity. In fact, all the strategic patterns have many benefits, including identifying whether a UL should be defined and whether the tactical patterns should be used at all. Subdomains can help break down complex problem domains to aid communication and identify what is important. Context maps reveal integration points between different contexts along with the relationships between teams. However, it is sometimes difficult to justify the tactical patterns of applying the domain model pattern to anything other than a complex or constantly evolving domain.
软件开发论坛上最常见的问题之一是:我可以看看示例应用程序吗?可能有很多优秀的解决方案可以展示在 DDD 流程下开发的产品的结果,但仅仅检查代码工件并不能充分展现 DDD 的诸多优势。DDD 的实践往往是在白板上、咖啡里,以及在走廊里与业务专家一起进行的;当一些小的重构突然揭示出一个隐藏的领域概念,并由此提供更深入洞察的关键时,它就显现出来了。示例应用程序无法展现领域专家与开发团队之间的多次对话和协作。
One of the most often-asked questions on software development forums is this: Can I see a sample application? There are probably many good solutions that show the result of a product developed under a DDD process, but much of the benefit of DDD is not revealed when you only examine the code artifact. DDD is performed on whiteboards, over coffee, and in the corridors with business experts; it manifests itself when a handful of small refactorings suddenly reveal a hidden domain concept that provides the key to deeper insight. A sample application does not reveal the many conversations and collaborations between domain experts and the development team.
代码工件是数月辛勤工作的成果,但它仅代表最后一次迭代。代码本身在达到今天的样子之前,可能已经经历了多次修改。随着时间的推移,代码将继续演进以支持不断变化的业务需求;今天有用的模型可能与产品未来迭代中使用的模型截然不同。
The code artifact is the product of months and months of hard work, but it only represents the last iteration. The code itself would have been through a number of guises before it reached what it resembles today. Over time, the code will continue to evolve to support the changing business requirements; a model that is useful today may look vastly different to the model used in future iterations of the product.
如果您希望模仿 DDD 的理念,并以此来审视一个按照 DDD 方法构建的解决方案,那么您将无法体会到其中的很多原则和实践,而且会过于强调代码的构建块。事实上,如果您不熟悉领域,您就会发现底层领域模型的表达能力并不强。
If you were to view a solution that had been built following a DDD approach hoping to emulate the philosophy, a lot of the principles and practices would not be experienced, and too much emphasis would be placed on the building blocks of the code. Indeed, if you were not familiar with the domain, you would not find the underlying domain model very expressive.
DDD 确实规定了一系列设计最佳实践、模式和构建块,这些常常被误认为是将 DDD 应用于产品的核心。相反,可以考虑这些设计工件仅仅作为一种手段来表示概念模型。DDD 的核心在于开发团队和领域专家之间的协作,以生成有用的模型。
DDD does prescribe a set of design best practices, patterns, and building blocks that are often mistakenly thought to be core to applying DDD to a product. Instead, think of these design artifacts as merely a means to an end used to represent the conceptual model. The heart of DDD lies deep in the collaboration between the development team and domain experts to produce a useful model.
团队过于关注战术模式,就会忽略 DDD 的精髓。DDD 的真正价值在于创建一种特定于特定场景的共享语言,使开发人员和领域专家能够有效地协作解决问题。代码是这种协作的副产品。消除对话中的歧义,实现轻松沟通才是最终目标。这些基础必须在任何编码工作开始之前就已打下,以便团队能够更好地解决问题。当开发真正开始关注语言、场景和协作时,代码才能井然有序,并与业务的思维模型紧密结合。
A team focusing too much on the tactical patterns is missing the point of DDD. The true value of DDD lies in the creation of a shared language, specific to a context that enables developers and domain experts to collaborate on solutions effectively. Code is a by-product of this collaboration. The removal of ambiguity in conversations and effortless communication is the goal. These foundations must be in place before any coding takes place to give teams the best chance of solving problems. When development does start to focus on language, context and collaboration enable code to be well organized and bound to the mental models of the business.
问题的解决不仅体现在代码中,还体现在与领域专家的协作、沟通和探索中。评判开发人员的标准不应该是他们编写代码的速度,而应该取决于他们解决问题的能力。
Problems are solved not only in code but through collaboration, communication, and exploration with domain experts. Developers should not be judged on how quickly they can churn out code; they must be judged on how they solve problems.
上下文、上下文、上下文,还是更多上下文。这是 DDD 中的一个基本概念。上下文可以帮助您组织大型问题域的解决方案。并非所有问题都能用同一个模型来解决。需要创建各种模型来解决不同的问题。在定义的上下文边界内创建模型对于保持代码可管理并避免其变成“大泥球”(BBoM)至关重要。了解上下文的结束和开始位置是上下文图的职责。如果没有上下文概念和上下文图的指导,团队将无法交付价值,因为他们需要不断地与混乱无序的代码库作斗争。
Context, context, context, and more context. It’s a fundamental concept in DDD. Context helps you organize solutions for large problem domains. All problems cannot be solved using the same model. Various models need to be created to solve different problems. Creating models within defined context boundaries is essential to keep your code in a manageable state and avoid it turning into a Big Ball of Mud (BBoM). Understanding where contexts end and begin is the responsibility of a context map. Without the notion of context and a context map to guide you, teams cannot deliver value because they are constantly fighting the unorganized mess of their codebase.
如果团队不了解其他环境,他们所做的更改可能会影响到其他环境。如果团队不清楚模型的边界,就有可能破坏其概念的完整性。模型之间适用性的界限模糊或缺失,往往会导致模型合并,从而很快形成BBoM。上下文图对于理解边界线以及如何维护模型的完整性至关重要。
If teams don’t understand other contexts, changes they make may bleed into those other contexts. Teams without a clear understanding of the boundaries of a model risk violating its conceptual integrity. Blurred or no lines of applicability between models often results in a merging of models, which quickly leads to a BBoM. A context map is vital to understanding boundary lines and how to uphold the integrity of models.
造成遗留代码和技术债务的最大问题在于代码的组织方式。代码编写起来很容易,但如果不仔细考虑其结构,阅读起来就会变得极其困难。了解上下文可以帮助您隔离不相关的概念,从而使模型更加纯粹和专注。这就像在架构层面应用单一职责原则 (SRP)。更易于维护和阅读的代码将使团队能够更快地交付价值,这正是 DDD 的精髓。
The biggest issue that contributes to legacy code and technical debt is how it’s organized. Code is easy to write, but without due care and attention to how it is structured, it can become extremely hard to read. Understanding about context enables you to isolate unrelated concepts so that models are more pure and focused. Think about it like applying the Single Responsibility Principle (SRP) but at an architectural level. Code that is easier to maintain and read will allow teams to deliver value faster, which is the essence of DDD.
识别领域专家在不同语境下讨论但仍使用相同术语的情况。如果在业务中使用相同的术语,很容易陷入认为模型可以重复使用的陷阱。警惕用于多个语境的隐式模型。最好创建显式模型。将“不要重复自己”(DRY)原则应用于有界上下文仅仅是一个概念,而不是一个系统。不要害怕在不同的语境中使用相同的概念和名称。团队需要知道的最重要的事情是,他们应该保护自己的边界。
Recognize when domain experts are talking in a different context but still using the same terms. If the same terms are used within the business, it is easy to fall into the trap of thinking that the models can be reused. Beware implicit models that are used for more than one context. It’s better to create explicit models. Apply the principle of Don’t Repeat Yourself (DRY) to a bounded context only and not a system. Don’t be afraid to use the same concepts and names in different contexts. The most important thing teams need to know about is that they should protect their boundaries.
有效的沟通对于理解和解决问题领域内的挑战至关重要。如果没有良好的沟通,开发团队和领域专家之间的协作就无法蓬勃发展。不重视共享语言必要性的团队可能会使用技术抽象,并使用他们自己的共享技术语言构建模型。当团队寻求帮助或验证其模型时,他们需要将其翻译成领域专家能够理解的形式。在最好的情况下,这种翻译会成为开发的瓶颈;在最坏的情况下,它可能导致团队围绕不重要或领域专家不理解的概念和主题进行构建。
Effective communication is essential for understanding and solving challenges within the problem domain. Without strong communication, collaboration between the development team and domain expert cannot flourish. Teams that do not value the need for a shared language are likely to employ technical abstractions and build a model using their own shared technical language. When the teams seek help or validation on their model, they are required to translate it for domain experts to understand. At best, this translation is a bottleneck for development; at worst, it can end up with the team building around concepts and themes that are not important or that the domain experts do not understand.
没有共享的语言,就无法创建共享的模型。这种对问题的共同愿景能够捕捉隐含的概念,并协作解决问题。创建语言的过程是开发团队和领域专家协作的直接结果。能够轻松解决问题并理解问题领域正是回报的来源。
Without a shared language, you cannot create a shared model. This shared vision of the problem enables the capturing of implicit concepts and collaborative problem solving. The process of creating a language is a direct result of collaboration between the development team and domain experts. Being able to easily solve problems and understand the problem domain is where the payoff comes from.
如果没有 UL,上下文就很难发现,因为有界上下文主要由语言的适用性定义。没有上下文和明确语言的模型很快就会变成 BBoM,因为同名概念会被建模成一个。
Without a UL, contexts are hard to discover, because a bounded context is primarily defined by the applicability of language. Models created without context and explicit language quickly turn into a BBoM as concepts with the same name are modeled as one.
语言的制定会对业务和产品开发产生重大影响。它有助于明确定义通用概念,并且像模式语言一样,在讨论复杂的领域逻辑和业务功能时消除歧义。
The formulation of a language can have a big impact on a business and product development. It helps to explicitly define common concepts, and just like a pattern language, remove ambiguity when talking through complex domain logic and business capabilities.
有了 UL,领域专家可以在实施领域模型时像开发团队一样提供软件问题的解决方案。
With a UL, domain experts can offer solutions to software problems when implementing the domain model as much as the development teams themselves.
如果没有协作,问题领域的关键概念可能会被忽略,而一些易于理解的概念可能会被过分强调。在组织内部,领域的重要方面往往是隐含的;没有与领域专家合作的团队可能会忽略这些方面,转而关注领域名词等更容易理解的内容。如果没有协作来验证理解并揭示暗示的概念,开发团队将围绕技术术语进行抽象,而业务用户将需要翻译才能理解解决方案领域中的问题与问题领域之间的关系。
Without collaboration, key concepts of the problem domain can be missed, and easy-to-understand concepts can be overemphasized. Within an organization, important facets of a domain are often implicit; teams not working with domain experts can overlook these, instead focusing on the lower-hanging fruit like the nouns of a domain. Without collaboration to validate understanding and reveal hinted-at concepts, development teams will abstract around technical terms, and business users will require translation to understand how the problems in the solution space relate to the problem space.
协作的意义在于让许多持有不同观点的人齐心协力,共同创建一个可用于解决问题的问题域模型。没有人拥有绝对的权威,也没有哪个建议是愚蠢的。
Collaboration is all about getting lots of people with different points of view working on creating a model of the problem domain that can be used to solve problems. No one has the authority on a good idea, and no suggestion is stupid.
试图与领域专家以外的任何人合作进行知识处理可能会徒劳无功。可以充当领域专家代理的业务分析师将能够向您提供需求并传达输入和输出,但他无法协助塑造模型来满足用例。
Trying to collaborate on knowledge crunching with anyone other than a domain expert can be a wasted effort. A business analyst who may act as a proxy for a domain expert will be able to give you requirements and communicate inputs and outputs, but he will not be able to assist with shaping a model to fulfill the use cases.
团队必须理解他们选择开发软件而不是集成现成解决方案的根本原因。理解战略愿景以及选择自主开发而非购买,有助于团队集中精力。如果不专注于项目成功的核心要素,资源有限的团队就不会将资源投入到最重要的领域——核心领域。将资源过度分散在项目最重要的领域是一种反模式。
Teams must understand the underlying reason why they are developing software instead of integrating an off-the-shelf solution. Understanding the strategic vision and the choice of build over buy helps teams concentrate effort. Without a focus on what is core to the success of a project, teams with a limited resource will not apply that resource in the most important areas — the core domains. Spreading resources too thin in the most important areas of a project is an antipattern.
如果没有清晰且共享的产品目标愿景(通常使用领域愿景声明来表达),软件设计就会受到影响。当开发人员理解业务用户需求背后的意图时,他们就能做出明智且有根据的设计决策。忽视业务意图并盲目开发会导致糟糕的结果。
The design of software will suffer without a clear and shared vision of the goal of the product, often captured using a domain vision statement. Well-informed and educated design decisions can be made when developers understand the intent behind business users’ requirements. Missing the intent of the business and blindly developing leads to poor results.
接受并非所有系统都能设计得好,也并非所有系统都需要设计得好,对团队来说,这是一大进步。如果不专注于系统的关键方面,团队中的优秀成员可能会被框架分散注意力,转而专注于最新的 JavaScript 框架的展现层,而不是产品的核心部分。
Accepting that not all of a system will be well designed and that not all of a system needs to be well designed is a big step forward for a team. Without a focus on the key aspects of a system, talented members of a team may be distracted by frameworks and instead want to work on the latest JavaScript framework at the presentation layer instead of the core aspects of a product.
除了团队之外,领域专家对核心领域的清晰理解也至关重要。如果领域专家不认同利益相关者的愿景,或者不确定软件的编写目的,那么在知识交流环节中,他们可能会因为消极或困惑而无法有效沟通。
In addition to teams, it’s important that domain experts have a clear understanding of the core domain. A domain expert who does not share the vision of a stakeholder or is unsure why the software is being written will not be effective during knowledge-crunching sessions due to negativity or confusion.
将旨在管理复杂问题的技术应用于复杂性较低或完全没有复杂性的领域,往好了说是浪费精力,往坏了说是会产生不必要的复杂解决方案,而且由于多层抽象,这些解决方案难以维护。DDD 最适合用于具有战略重要性的应用程序;否则,在 DDD 过程中获得的深厚知识对组织的价值微乎其微。
Applying techniques designed to manage complex problems to domains with little or no complexity will result in at best wasted effort and at worst needlessly complicated solutions that are difficult to maintain due to the multiple layers of abstractions. DDD is best used for strategically important applications; otherwise, the deep knowledge gained during DDD provides little value to the organization.
在创建系统时,开发人员应力求简洁明了。充满抽象的软件除了满足开发人员的自负和掩盖简单代码库的本质之外,别无他用。那些不致力于交付价值、只专注于技术工作的开发人员会因为对业务问题感到厌倦而制造复杂性。这种软件设计可能会让未来需要维护混乱技术层的团队感到沮丧。
When creating a system, developers should strive for simplicity and clarity. Software that is full of abstractions achieves little more than satisfying developers’ egos and obscuring the reality of a simple codebase. Developers who aren’t engaged with delivering value and are instead only focused on technical endeavors will invent complexity because they’re bored by the business problem. This kind of software design can lead to frustration for teams in the future that need to maintain the mess of technical layers.
简单的问题需要简单的解决方案。那些对企业没有战略优势的琐碎领域或子领域,无法从 DDD 的所有原则中获益。那些热衷于将 DDD 原则应用于任何项目(无论问题领域的复杂程度如何)的开发人员,很可能会遇到业务同事的不满,因为他们对那些不太重要的业务领域漠不关心。
Simple problems require simple solutions. Trivial domains or subdomains that do not hold a strategic advantage for businesses will not benefit from all the principles of DDD. Developers who are keen to apply the principles of DDD to any project regardless of the complexity of the problem domain will likely be met with frustrated business colleagues who are not concerned with the less important areas of a business.
DDD 是一套原则,旨在帮助您管理对业务具有重大优势的复杂问题域。业务预期较低的问题应该以平淡无奇的方式处理。这并不是说这些问题应该以杂乱无章的方式构建。相反,这些问题应该构建得高性能且易于维护,但逻辑性较差的问题则需要直接的解决方案。
DDD is a set of principles to help you manage complex problem domains that hold significant advantage for a business. Problems with a low business expectation should be tackled in a no-thrills manner. This is not to say that they should be built in a haphazard manner. On the contrary, they should be built to be performant and maintainable, but problems that have little logic need straightforward solutions.
大型复杂系统会包含许多子域,其中一些包含对产品战略重要性至关重要的复杂逻辑,而另一些则仅仅是用于管理数据的表单,复杂度很低甚至没有。DDD 的战术模式以及协作构建 UL 来传达模型应该只用于核心子域。这些领域需要清晰地理解,以支持快速变更,或用于建模复杂且内在的逻辑。团队不应在通用域或子域上浪费精力,而应确保它们能够正常工作并与核心域隔离。
Large complex systems will have many subdomains, some containing complex logic key to strategic importance of a product, whereas others will simply be forms to manage data with little or no complexity. The tactical patterns of DDD along with collaborating to build a UL to communicate models should be reserved for the core subdomains only. These are the areas that need to be clear to aid rapid change or that model complicated and intrinsic logic. Teams should not waste energy on the generic domains or subdomains beyond keeping them working and isolated from the core domains.
并非所有系统都设计精良;试图完善整个代码库可能会徒劳无功。你的注意力和精力应该放在核心领域,因为其他任何好的部分都足够好。对于领域逻辑很少或没有领域逻辑、数据结构仅包含表单的系统,可以选择更简单的架构,例如基于创建、读取、更新和删除 (CRUD) 的系统,以减少时间投入并提高核心领域的可用性。
Not all of your system will be well designed; trying to perfect an entire codebase can be wasted effort. Your focus and energy should be on the core domain, for anything else good is good enough. For systems with little or no domain logic and with no more than forms over data opt for a simpler form of architecture such as a create, read, update, and delete (CRUD)-based system to decrease time spent and increase availability for the core domain.
领域模型模式对于复杂或频繁变化的模型非常有用。对于通用或缺乏领域逻辑的模型,采用领域模型模式所需的工作量将远远大于其获得的价值。对于核心领域,应使用模型驱动设计和领域模型模式,而对于系统中较简单的部分,则使用其他领域逻辑模式。
The domain model pattern is useful for complex or frequently changing models. The effort required to employ the domain model pattern for models that are generic or lack domain logic will be far greater than any value that will be gained. Utilize model-driven design and the domain model pattern for the core domain, and use other domain logic patterns for simpler parts of your system.
初级开发人员学习设计模式时,会试图将其应用到自己编写的每一段代码中。这种行为在团队学习领域驱动设计 (DDD) 时也经常出现。他们只关注 DDD 的战术模式,盲目地将这些模式应用到每个项目中,无论其合理性如何。这种急于应用新理念而不充分考虑该流程是否值得为软件付出的努力,可能会导致不必要的复杂性,而其实一个简单的解决方案就足够了。
When junior developers learn about design patterns, they try to apply them to every piece of code they write. This behavior is often seen when teams learn about DDD. They focus only on the tactical patterns of DDD, blindly applying these patterns to every project regardless of whether it is justified. This eagerness to apply a new philosophy without due consideration as to whether the process is a worthwhile endeavor for the software can lead to needless complexity where a simple solution would have sufficed.
如果现成的软件就足够了,就不要开发非战略性的软件。如果将手动流程自动化太费力,那就直接手动操作吧。记住:解决方案不一定非得是技术性的。
Don’t develop nonstrategic software if off-the-shelf software will suffice. If the effort to automate a manual process is too great, just leave it manual. Remember: Solutions don’t always have to be technical.
应用 DDD 原则既困难又耗费时间和资源。最好只将它们完全应用于系统中最重要的领域:核心领域。这些原则取决于企业是否愿意与您合作解决问题,而不是让您孤立地工作。DDD 通常对产品设计中的非技术部分更有价值。
Applying the principles of DDD is hard and costly both in time and resource. It makes sense to only fully apply them to the most important areas of your system: your core domain. The principles hang on a business willing to work with you on solutions rather than have you work in isolation. DDD often is more valuable to the nontechnical parts of product design.
DDD 并非适合所有人。它当然也并非适合所有项目。为了在遵循 DDD 的过程中获得最大收益,您需要一个可以长期投入的复杂核心领域、一个迭代式开发流程,以及与领域专家的沟通。然而,这并非全部。您还需要掌握许多其他技能才能在 DDD 中取得成功。为了有效地运用 DDD,您需要具备以下技能:
DDD is not for everyone. It is certainly not the right fit for every project. To gain the most benefit when following DDD, you need a complex core domain that will be invested in over time, an iterative development process, and access to domain experts. However, that is not all that is required. There are a number of other skills that you need to succeed at DDD. To be effective at DDD, you need the following:
您需要训练有素的开发人员,他们愿意与领域专家合作并了解业务,而不是担心如何将最新的 JavaScript 框架融入到项目中。
You need disciplined developers who are willing to work with domain experts and understand the business rather than worry how they can wedge the latest JavaScript framework into a project.
记住:DDD 并非灵丹妙药。正如从前期瀑布式开发方法转向更敏捷/XP 的项目方法并不能解决所有软件开发问题一样,选择遵循 DDD 也不会一下子就创造出更好的软件。任何成功项目的共同点在于拥有一支由聪明人组成的团队,他们对自己的工作充满热情,并致力于项目的成功。
Remember: DDD isn’t a silver bullet. Just as switching from an upfront waterfall approach to a more agile/XP project methodology didn’t solve all your software development problems, opting to follow DDD won’t suddenly produce better software. The common denominator in any successful project is a team of clever people who are passionate about what they are doing and who care about it succeeding.
业务所有权和投资,尤其是来自领域专家的投入,是成功合作的关键。如果开发团队与那些对项目缺乏投入或不理解项目意图或愿景的领域专家合作,他们就不太可能找到有用的模型、创建最终用户指南 (UL),也无法组成一支高效的团队。开发团队擅长设计系统来应对问题领域的挑战。然而,当与领域专家合作时,他们可以更进一步,通过从源头上解决问题并重新定义业务流程,共同努力,从而消除对软件解决方案的需求。
Business ownership and investment especially from domain experts is key to successful collaboration. If a development team is working alongside domain experts who are not invested in the project or do not understand the intent or vision, they will unlikely discover a useful model, create a UL, and work as an effective team. Development teams are great at designing systems to handle challenges in the problem domain. However when collaborating with a domain expert, they can go further and work together to remove the need for software solutions by removing issues at the source and redefining business processes.
DDD 的核心在于头脑风暴,在于协作学习。它意味着不要止步于你的第一个想法,而是要不断尝试,直到发现更好的方案,或者仅仅验证你最初的想法。所有这些都需要时间,不支持这些方法论就无法支持 DDD。
DDD is about brainstorming. It’s about collaborative learning. It’s about not stopping at your first idea but continuing to experiment so you discover something better or simply to validate your initial idea. All this takes time, and a methodology that doesn’t support this can’t support DDD.
一次尝试不可能创造出一个有用的模型;因此,需要一种迭代开发方法来完善设计。模型会不断发展。如果团队不理解模型和语言仅在特定时间内有效,他们创造的有用成果很快就会变成一堆BBoM。模型需要呵护。随着对领域洞察的加深以及新用例对模型的挑战,模型需要不断完善和重构。
A useful model will not be created on the first attempt; therefore, an iterative development methodology is required to hone a design. Models evolve. Teams that don’t appreciate that the model and language are only valid for a given time will quickly see their useful creation turn into a BBoM. A model needs love. It needs to be refined and refactored as more insight into the domain is gained and as new use cases challenge the model.
值得注意的是,为了实验和改进模型,你需要单元测试的安全性。这并不是说你必须遵循测试驱动的流程;而是你必须确保你的代码在一系列重构之后仍然有效。
It’s also worth noting that, to experiment and evolve a model, you need to have the safety of unit tests. That is not to say that you must follow a test-driven process; instead, you must ensure your code can be valid after a series of refactors.
每个解决方案都必须对应一个合适的问题。DDD 是一种适用于特定问题空间的设计理念。它是你工具箱中必备的利器。但是,如果您的业务并不复杂或变化不频繁,那么不要轻易动用 DDD:记住,你的开发工具包中还有其他更合适的工具。只需将你的建模工作和 DDD 重点放在最复杂的、能为客户带来最大价值的有界上下文上即可。
For every solution, there must be an appropriate problem. DDD is a design philosophy suited to a particular problem space. It is a great tool to have in your toolbox. However, if your business is not complex or isn’t changing frequently then don’t automatically reach for the DDD hammer: remember there are other better suited tools in your development tool kit. Only focus your modeling efforts and DDD on the most complex bounded contexts that bring the most value to your customer.
并非所有子域都很复杂。有些域或上下文甚至可能不需要完整的域模型,可能只包含一些没有业务逻辑的数据,只需要基本的 CRUD 操作。对于低复杂度的上下文,建议使用基于 CRUD/Active Record/事务脚本的应用程序,并将 DDD 的战术模式留给系统中对客户重要、复杂且频繁变化的部分。
Not all subdomains are complex. Some domains or contexts may not even need a fully fledged domain model and may just contain data with no business logic that simply requires the basic CRUD operations. For low-complexity contexts, favor the use of a CRUD/Active Record/Transaction Script-based application, and leave the tactical patterns of DDD for parts of your system that are important to your customer, that are complex, and that change frequently.
问问自己:这些额外的努力是帮助你交付解决方案,还是拖慢了你的进度?保持简单,但不要过于简单。不要过度设计解决方案,也不要试图利用无用的框架。保持简单是一门艺术,需要练习和务实的心态。
Ask yourself: Is this extra effort helping you deliver your solution, or is it slowing you down? Keep things simple but not simplistic. Don’t over engineer a solution or try to leverage unhelpful frameworks. Keeping things simple is an art form and takes practice and a pragmatic mind-set.
不要在不必要的领域追求完美。对于通用或支持子域,请保持简单、直接和不复杂。使用 CRUD 和简单的域逻辑模式。编写可运行的代码;然后转到核心域。核心域是您可以追求完美的领域。在不重要的限界上下文中,小泥球有时效果更好;它们可以快速编写代码并将其排除在外。如果您需要更改它,您可以覆盖它。对于产品中不重要、不太可能更改或随着时间的推移投入的领域,优先考虑可运行的代码而不是完美的代码。好的通常就足够了。不要担心您是否做得对或开始寻求确认;这会浪费精力并分散注意力。将纯粹性留给重要的领域。
Don’t try to strive for perfection in areas that don’t need it. For generic or supporting subdomains, keep things simple, straightforward, and uncomplicated. Use CRUD and simple domain logic patterns. Get the code written so it works; then move on to the core domain. The core domain is the area where you can strive for perfection. Small balls of mud are sometimes better in bounded contexts that are unimportant; they get the code written quickly and get it out of the way. If you need to change it, you can overwrite it. For areas of your product that are unimportant, unlikely to change or be invested in over time, favor working code over perfect code. Good is often good enough. Don’t worry if you are doing it right or start to seek confirmation; this will be wasted effort and a distraction. Leave purity for the areas that count.
当你遵循 DDD 原则构建系统时,你不会收到 Eric Evans 邮寄的证书来祝贺你的成就。盲目遵循任何模式、语言或方法论而不考虑自身独特的环境都是愚蠢的。仅仅为了遵循某种方法论而试图遵循一套规则是一种反模式。DDD 哲学并非遵循一套规则或应用编码模式。它是一个学习的过程。学习的过程远比目标重要,而学习的重点在于与领域专家合作探索你的问题领域,而不是在解决方案中运用多少设计模式。
When you build a system following the principles of DDD, you do not receive a certificate in the post from Eric Evans congratulating you on your achievement. Blindly following any patterns language or methodology without considering your own unique context is foolhardy. Trying to adhere to a set of rules for no other reason than to compile with a methodology is an antipattern. The DDD philosophy is not about following a set of rules or applying coding patterns. It is a process of learning. The journey is far more important than the destination, and the journey is all about exploring your problem domain in collaboration with domain experts rather than how many design patterns you can employ in your solution.
搜索论坛并阅读 DDD 博客文章,了解团队如何与业务部门协作,以促进学习并增加发现。不要试图创建完美的存储库模式,也不要向未直接参与项目的同事寻求确认,因为如果没有完整的背景信息,您必须轻视任何建议。
Search forums and read DDD blog posts to discover how teams are collaborating with the business to aid learning and increase discoveries. Don’t try to create the perfect repository pattern, and don’t seek confirmation from your peers who aren’t involved directly in your project because without the full context, you must take any advice lightly.
那些执着于应用设计模式和原则而不顾实际需求的团队,很可能会创建过于复杂且令人困惑的架构,从而从一开始就偏离产品的目标。团队应该理解设计模式背后的动机,并明智地运用它们。盲目地采用 DDD 的战术模式,对业务没有任何价值。
Teams that obsess with applying design patterns and principles regardless of the actual need will likely create overly complex and confusing architectures that miss the goal of the product in the first place. Teams should understand the motivations behind design patterns and use them judiciously. Blindly employing the tactical patterns of DDD does nothing to add value for the business.
在频繁变化的重要领域进行灵活的设计,有助于模型的灵活性和演进能力,而不会产生大的连锁反应。在那些几乎没有商业价值且不会被投入的领域,煞费苦心地追求优雅的设计,纯粹是浪费精力。与其费力地追求处处追求优美的代码,不如将一些小的“泥球”与其他易于替换的上下文隔离开来。
A supple design in important areas that frequently change aids a model’s ability to be flexible and evolve without having large rippling effects. Painstakingly striving for elegant design in areas that offer little business value and will not be invested in is a waste of efforts. It is far better to have small balls of mud, isolated from other contexts that can easily be replaced, rather than trying to strive for beautiful code everywhere.
在核心领域工作时,团队在确定某种模式和思维方式之前,当然应该等待一段时间。推迟重构,并尝试使用代码来发现哪些因素最容易导致摩擦/变化,可以揭示更多关于该领域的信息,并最终做出更明智的设计选择。
When working in the core domain, teams should certainly wait before committing to a pattern and a way of thinking. Delaying refactoring and living with the code to see what causes friction/changes the most can reveal more about the domain and lead to a more informed design choice.
不要被模式、框架或方法论所迷惑;它们只是实现细节。你的目标是更深入地理解你的领域,以便更好地解决其中的问题。这才是 DDD 的真正价值。
Don’t be distracted by patterns, frameworks, or methodologies; they are implementation details. Your goal is to understand your domain at a deeper level to be best equipped to solve problems within it. This is the true value of DDD.
不要让设计模式和原则妨碍你完成任务并为业务创造价值。模式和原则是指导你创建灵活设计的指南。在应用程序中使用它们的次数越多,荣誉勋章就越少。DDD 的重点是提供价值,而不是编写优雅的代码。
Don’t let design patterns and principles get in the way of getting things done and providing value to the business. Patterns and principles are guides for you to produce supple designs. Badges of honor will not be given out the more you use them in an application. DDD is about providing value, not producing elegant code.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第十章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 10 download and individually named according to the names throughout the chapter.
在前九章中,你已经对领域驱动设计 (DDD) 的理念有了大致的了解。本章将整合所有这些知识,向你展示如何将 DDD 的原则和实践应用到你的下一个项目中。本书的其余部分将重点介绍编码模式,以便你能够以代码形式生成有效的领域模型、集成有界上下文以及构建可维护的应用程序。
Over the previous nine chapters, you have gained an overview into the philosophy of Domain-Driven Design (DDD). This chapter brings all of that knowledge together to show you how you can start to apply the principles and practices of DDD to your next project. The remainder of this book focuses on coding patterns to produce an effective domain model in code, integrate bounded contexts, and architect maintainable applications.
DDD 并非灵丹妙药,也不应该被当作灵丹妙药来推销。正如遵循敏捷方法论并不能解决所有问题一样,DDD 也一样;然而,在正确的情况下使用,它是一种强大且极其有效的理念,例如:
DDD is not a silver bullet, and it shouldn’t be sold as one. In the same way that following an agile methodology won’t solve all of your problems, neither will DDD; however, it is a powerful and extremely effective philosophy when used in the correct circumstances, such as these:
如果没有这些关键要素,应用 DDD 的原则和实践将使您的开发工作过于复杂,而不是简化它。但是,如果您的具体情况符合上述条件,那么应用 DDD 的原则和实践可以极大地提升您的开发工作的价值。
Without these key ingredients, applying the principles and practices of DDD will overcomplicate your development effort rather than simplify it. However, if your particular circumstances meet the preceding list, then applying the principles and practices of DDD can greatly increase the value of your development effort.
不要把 DDD 当作一种项目方法论来推销;相反,要理解并恰当地运用这些原则,并根据自身情况获得价值。正如设计模式最好在重构时形成一样,DDD 的原则和实践也应该只在必要时使用,并且各有优缺点。任何技术都要谨慎应用,并且只在能够带来优势并带来价值时才使用。
Don’t sell DDD as a project methodology; instead, understand and apply the principles appropriately and where you can gain value. Just as design patterns are best arrived at when you refactor, the principles and practices of DDD should be used only when necessary, and with each on its own merit. Apply any technique judiciously and only when you can gain an advantage and it can give you value.
在向团队传达 DDD 的理念时,重点不应放在模式语言上,而应放在与业务的契合度、战略环境的重要性以及用于描述模型的语言上。战略 DDD 的原则和模式远比战术模式强大得多,也更有用。一个只关注软件模式的团队会错失 DDD 的真正价值,并且很可能付出应用这些实践的成本而得不到回报。
When communicating the philosophy of DDD to your team, focus not on the pattern language but instead on the alignment with the business, the importance of strategic contexts, and the focus on language used to describe a model. The principles and patterns of strategic DDD are far more powerful and useful than the tactical patterns. A team focused solely on the software patterns will miss the true value of DDD and will likely pay the cost of applying the practices without reaping the rewards.
技术并非解决业务问题的方案,它仅仅是一个实现细节。问题解决需要与领域专家合作,他们掌握着发现有用模型的关键。该模型在卡片或白板上进行头脑风暴,然后在 Visual Studio(或您最喜欢的 IDE)中实现。请记住:技术可能会使问题解决变得复杂;专注于一个不受基础设施和技术问题困扰的抽象概念模型,将使团队能够构建一个能够传达领域意图并被业务部门理解的解决方案。
Technology is not the solution to business problems; it is merely an implementation detail. Problem solving is achieved through collaboration with domain experts who hold the key to discovering a useful model. This model is brainstormed on cards or a whiteboard before being implemented in Visual Studio (or your favorite IDE). Remember: Technology can complicate problem solving; focusing on an abstract conceptual model free from the clutter of infrastructure and technical concerns will enable teams to build a solution that communicates the intent of the domain and can be understood by the business.
您的业务利益相关者并不想听到最新的开发理念或方法;他们只希望您能为他们创造价值。敏捷、面向服务架构 (SOA) 和云计算都被过度炒作,承诺解决所有开发问题,但却未能如愿。相反,您应该只谈论您希望深入了解您所在的业务,从而创造更多价值。谈论开发团队需要更加贴近业务愿景和目标。利益相关者会看到开发团队花时间与业务专家沟通,并根据业务期望进行调整的价值。
Your business stakeholders don’t want to hear about the next new development philosophy or methodology; they just want you to deliver value for them. Agile, Service-Oriented Architecture (SOA) and the cloud have all been overhyped, promising to solve all development problems but failing to do so. Instead, talk only of your desire to learn more about the business you work within to give more value. Talk of the need for the development team to be more aligned to the vision and intent of the business. A stakeholder will see the value in the development team spending time with business experts and aligning themselves to the expectations of the business.
如果没有领域专家的投入,DDD 就无法奏效。如果您能向利益相关者清晰地阐明聘请业务专家的重要性,那么您就为成功做好了准备。当然,您可以自由地与利益相关者讨论 DDD,但最好专注于协作的需求。产品的成功取决于业务部门及其专家的投入程度;这就是您推广 DDD 的方式。让团队深入了解问题域逻辑,能够帮助他们打造出更优秀的产品。
DDD does not work without the commitment of domain experts. If you can articulate the importance of having an expert from the business available to your stakeholders, you are setting yourself up to succeed. Of course, you are free to talk to your stakeholders about DDD, but it’s best to focus on the need for collaboration. The success of a product falls on the commitment level of the business and its experts; this is how you sell DDD. Empowering a team with a deep understanding of the problem domain logic enables it to produce a better product.
如果企业了解领域专家时间的价值,并且开发团队专注于围绕与领域专家合作创建的领域模型推动复杂问题的解决方案,那么您将能够很好地应用 DDD 原则。本节详细介绍了从项目启动到开发解决方案的各个阶段,以及何时使用哪些实践。但是,应用 DDD 的关键是从简单开始。尽可能做最简单的事情,直到遇到复杂性或歧义性。当您在对话中发现歧义时,请使用通用语言 (UL) 明确定义它。当您的模型变得过于庞大时,分解复杂性并应用代码组织和建模技术的战略模式。如果您保持简单并应用原则和实践,而不是试图用大锤砸坚果,那么您将从 DDD 中获得巨大的价值。
With a business that understands the value of a domain expert’s time and a development team that is focused on driving a solution to a complex problem around a model of the domain created in collaboration with a domain expert, you will be in a good position to apply the principles of DDD. This section details the stages of project inception to developing a solution, and which practices to use when. However, the key to applying DDD is to start simple. Do the simplest thing possible until you encounter complexity or ambiguity. When you find ambiguity during conversation, explicitly define it within the ubiquitous language (UL). When your model is becoming too large, decompose the complexity and apply the strategic patterns of code organization and modeling techniques. If you keep things simple and arrive at applying the principles and practices rather than trying to crack a nut with a sledgehammer, you will get immense value from DDD.
在收集需求之前,务必确保你的团队与利益相关者的期望保持一致。与利益相关者会面时,可以先提出一些开放式问题,阐明你选择自行开发而非购买产品的原因,并帮助解释产品的愿景。以下问题非常有用:
Before capturing requirements, it’s important to align your team with stakeholders’ expectations. Start a meeting with a stakeholder by asking open questions that draw out the reason you are opting to build rather than buy a product, and that help to explain the vision of the product. The following questions are powerful:
通过倾听利益相关者的提问,您可以确定产品中最重要的部分——软件领域,也就是您构建该产品的根本原因。捕捉这些信息,并创建一个领域愿景声明,让您的团队朝着共同的目标迈进。您的团队需要了解产品的成败因素、关键要素以及价值所在。
By listening to the stakeholder answering your questions, you can identify the most important part of the product—the area of the software that is the fundamental reason you are building it. Capture this information and create a domain vision statement that aligns your team to a common goal. Your team needs to understand what will make or break the product, what is essential, and where the value is.
在会议期间,您可能会发现该产品并非独一无二,实际上只是一个通用解决方案,只是内部开发更具成本效益。如果是这样,请明确说明这一点,并理解其对业务及其未来的重要性。也可能这是一款快速失败的产品——一个用来测试客户需求的原型。这些洞察将帮助您了解其价值所在以及业务目标。有了这些理解,您就可以确定所有 DDD 实践是否对您的项目有效。
During the meeting, you may discover that the product is not special and is in fact a generic solution that just happens to be more cost effective to build in-house. If so, make that explicit, and understand its importance to the business and its future. It could also be the case that this is a fail-fast product—a prototype to test the appetite of customers. These insights will enable you to understand where the value lies and what the business is trying to achieve. With this understanding, you can determine if all the practices of DDD are going to be effective for your project.
一旦您理解并分享产品的意图和利益相关者的目标,您就可以捕捉产品的特性,同时始终与产品的整体愿景保持一致。
Once you understand and share the intent of the product and the stakeholders’ goals, you can capture the features of the product while always aligning to the overall vision of the product.
在收集需求时,让利益相关者参与进来的一种有效方法是应用行为驱动开发 (BDD) 方法。BDD 是一种共享语言,可以帮助您捕获系统的行为。您可以将其视为需求的 UL。它使您的团队能够从非技术角度理解利益相关者的需求。实践 BDD 并专注于使用业务人员能够理解且能够明确体现价值的语言来捕获需求,这很好地展示了共享语言在促进沟通方面的强大作用。此过程有助于让团队了解语言的力量以及语义模糊的危害。利用这些环节来明确定义术语。此练习将为团队成员在 UL 上协作描述模型做好准备,该模型将实现满足系统行为所需的规则、逻辑和流程。
An effective way to engage stakeholders when gathering requirements is to apply the behavior-driven development (BDD) methodology. BDD is a shared language that helps you capture the behaviors of a system. You can think of it as a UL for requirements. It enables your team to understand the requirements from your stakeholder from a nontechnical perspective. The practice of BDD and the focus on capturing requirements in a language the business understands and which explicitly shows value is a great exercise in demonstrating the power of a shared language to aid communication. This process helps to educate the team on the power of language and the evil of ambiguity in meaning. Use these sessions to explicitly define terminology. This exercise will warm up the team members for when they collaborate on a UL to describe the model that will implement the rules, logic, and processes needed to satisfy the system’s behaviors.
你从利益相关者那里获取的是用例、输入和输出。这些业务用例构成了你的应用服务。如果它们很复杂,它们将决定你将实现哪种领域逻辑模式。在需求收集过程中,重点关注利益相关者的需求、时间以及原因。“为什么”至关重要。提出问题有助于验证利益相关者为什么想要他所说的内容。在需求阶段,请停留在问题空间,并专注于该产品将带来的机会。如何满足需求可以等到你理解并分享愿景之后再谈。
What you are capturing from the stakeholders are use-cases, inputs, and outputs. These business use cases form your application services. If they are complex, they drive the decision on what domain logic pattern you will implement. During requirements gathering, focus on what the stakeholder wants, when, and why. The why part is essential. Asking the question helps to validate why the stakeholder wants what he says. During the requirements stage, stay in the problem space, and focus on the opportunities that this product is going to bring. How you are going to satisfy the requirements can wait until you understand and share the vision.
我见过很多团队匆匆忙忙地处理需求,急于在未充分探究问题的情况下就直接跳到解决方案。不要操之过急地跳到解决方案;务必与利益相关者一起探索问题空间。利益相关者通常不清楚自己到底想要什么。通过探索问题空间,您可以找出真正的业务需求,并通常提供更好的行为,避免浪费时间去解决业务实际上并不存在的需求。
I have seen many teams rush through requirements, eager to jump to a solution without fully exploring the problem. Don’t jump to a solution too quickly; ensure that you explore the problem space with your stakeholders. Often, stakeholders are unclear on exactly what they want. By exploring the problem space, you can draw out the real business need and often offer better behaviors before wasting time on solutions to needs the business doesn’t really have.
当你开始生成充满功能的故事卡时,你可能会发现产品规模变得过于庞大,难以管理,或者问题空间中出现了模糊性。你也可能忽略了产品的整体情况以及开发该产品的核心原因。当这种情况发生时,就需要对问题空间进行提炼。
As you start to generate story cards full of features, you may find that the size of the product is becoming too big to manage or ambiguity is occurring in the problem space. You might also be losing sight of the bigger picture and the core reason that this product is being built. When this occurs, the problem space needs to be distilled.
如果您发现问题域变得过于庞大而难以管理,可以通过创建子域将问题抽象到更高层次来减轻认知负担。寻找支持产品的功能并基于这些功能创建子域非常有用。业务功能是支持业务流程的活动;请在部门结构或职能之外寻找这些活动。有些是通用的,有些是支持性的。真正决定产品成败的是核心域。
If you find that the problem domain is becoming too large to manage, you can ease cognitive load by abstracting the problem to a higher level by creating subdomains. It’s useful to look for the capabilities that support the product and create subdomains from these. Business capabilities are the activities that support a business process; look for these activities outside of departmental structures or functions. Some will be generic, some supporting. The ones of real interest that will make or break the product are the core domains.
分解问题空间后,请确保将大部分时间用于与利益相关者沟通,了解核心领域所需的行为。您的核心领域可能很小,这很正常。请特别关注这一领域的对话,因为它们最有趣,也最能为您带来价值。核心领域应该直接支持利益相关者的整体愿景;如果不是,则可能是您错误地识别了核心领域,或者您可能需要与利益相关者一起澄清愿景。
When you have decomposed your problem space, ensure that you spend the majority of your time with stakeholders understanding required behaviors for the core domain. Your core domain may very well be small, which is fine. Pay particular attention to conversations in this area, because they will be the most interesting and offer the most value to you. The core domain should directly support the overall vision that the stakeholders have; if it does not, you may have incorrectly identified the core domain, or you may need to clarify the vision with your stakeholders.
在彻底理解问题空间并明确系统价值所在后,您就可以开始构建解决方案了。然而,在开始为任何项目创建解决方案之前,至关重要的是要了解您将要工作的环境。了解已投入生产的软件解决方案的状态,对于就如何集成产品做出明智的决策至关重要。捕捉全局的最佳方法是创建上下文图。
With a thorough understanding of the problem space and an alignment on where the value of the system is, you can start to model a solution. However, before you start creating a solution to any project, it’s of utmost importance to understand the environment that you will be working in. Understanding the state of the software solutions already in production is essential to making informed decisions on how you will integrate your product. The best way to capture the landscape is by creating a context map.
团队需要识别正在运行的不同有界上下文,这些上下文会直接影响你的产品,或者受你的产品影响。团队需要确定这些上下文如何交互、它们通过哪些协议集成以及它们管理哪些数据。为此,请遵循以下步骤:
The team needs to identify the different bounded contexts in play that will directly affect or be affected by your product. The team needs to identify how these contexts interact, what protocols they integrate through, and what data they manage. To achieve this, follow these steps:
整个团队必须理解上下文图。把它挂在墙上,让所有团队成员都能看到,也让其他团队理解。这就是你的作战地图。它应该足够简单,以便所有人都能快速绘制,所以不要在UML图上花费太多时间。相反,要从整体上进行观察,当你开始整合时,放大接触点,然后再深入细节。
The whole team must understand the context map. Hang it on the wall for all the team to see and other teams to understand. This is your war map. It should be simple enough for all to draw quickly, so don’t spend too much time on UML diagrams. Instead, get a bird’s-eye view, and as you start to integrate, zoom in on touch points and then go into detail.
在开始建模解决方案并应用 DDD 原则之前,您需要确保您即将提供解决方案的产品满足以下标准:
Before starting to model a solution and applying the principles of DDD, you need to ensure the product you are about to provide a solution for meets the following criteria:
如果您的问题并不复杂,或者问题的一部分并不复杂,那么构建领域模型可能有些过度。当逻辑很少且仅涉及数据操作时,您应该采用更简单的方法来管理领域逻辑,例如事务脚本或 Active Record。这样可以避免一些成本更高的做法。
If you don’t have a complex problem or a portion of your problem is not complex, then building a domain model may be overkill. When there is little logic and merely data manipulation, you should follow a simpler method to manage domain logic, such as transaction script or active record. That way you can avoid some of the more costly practices.
如果产品对业务不重要,且预期不高,那么构建一个经得起时间考验的解决方案可能不值得。构建一个足够好的解决方案,并用于替换,而不是投资。您可以尝试使用现成的解决方案。如果您的问题比较普遍,那么可能已经有一个开源解决方案可以满足您的需求。
If the product is not important to the business and there are low expectations, it’s probably not worth the effort of building a solution to stand the test of time. Build good enough and build for replacement rather than investment. You can try and utilize an off-the-shelf solution. If your problem is generic there may well already be an open source solution out there that fits your needs.
如果你无法联系到领域专家,那么该领域的发现就无法实现。团队会围绕技术问题进行抽象,代码库中的语言也无法反映问题领域。上下文无法得到很好的定义,代码很快就会变得一团糟。
If you don’t have access to a domain expert, discoveries in the domain will not happen. The team will abstract around technical concerns, and the language in the codebase will not reflect the problem domain. Contexts will not be well defined, and quickly the code will evolve into a ball of mud.
如果您的团队没有动力或缺乏企业设计模式和原则的知识,那么最好采用更简单的模式来组织领域逻辑,这样就不会使开发工作过于复杂。
If your team is not motivated or lacks the knowledge of enterprise design patterns and principles, it may be best to favor a simpler pattern to organize domain logic so you don’t overcomplicate the development efforts.
并非所有问题领域都需要 DDD 的全部原则和实践。你必须有所选择。对于解决方案中不需要与领域专家协作的部分,不要让她参与。对于不需要领域模型模式在代码中表示抽象模型的部分,也不要创建这样的模式。
Not all parts of the problem space require the full spectrum of the principles and practices of DDD. You must pick your battles. For areas of your solution that don’t require the collaboration with a domain expert, don’t involve her. For areas that don’t require the domain model pattern to represent an abstract model in code, don’t create one.
如果您的问题空间的一部分很复杂,您的业务有很高的期望,您可以接触到领域专家和一支能够应对挑战的团队,那么您就有确切的标准来确定 DDD 可以帮助您管理产品开发。
If a portion of your problem space is complex, your business has high expectations, you have access to a domain expert and a team up to the challenge, you have the exact criteria for which DDD can help you manage the development of your product.
领域专家是指对问题领域拥有深厚知识的主题专家。利益相关者定义系统需要做什么,而领域专家则与开发团队协作,运用其洞察力、专业知识和经验来建模满足行为的解决方案。领域专家可以是当前系统的长期用户,对问题空间的流程和逻辑有着深入的了解。领域专家也可以是系统产品负责人,也可以仅仅是部门内在该领域工作多年的人。重点在于,“领域专家”并非指头衔;它是指企业中任何能够在问题领域提供专业知识的人。
A domain expert is a subject matter expert with deep knowledge of the problem domain. Whereas a stakeholder defines what the system needs to do, a domain expert collaborates with the development team, using his insight, expertise, and experience to model a solution that satisfies the behaviors. A domain expert could be a long-term user of a current system that has in-depth knowledge of the processes and logic of the problem space. The domain expert could equally be the systems product owner or simply someone who works in the department and has worked in the domain for many years. The point is that the term domain expert does not refer to a title; it’s anyone in the business who can offer expertise in the problem domain.
如果没有领域专家,DDD 就无法发挥作用。道理就这么简单。如果没有领域专家,很多洞见、丰富的领域知识和语言都无法被发掘。如果没有专家,开发团队可能会向现有系统的用户寻求建议,虽然他们了解现有流程,但可能并非提供颠覆性智慧或领域智能的最佳人选。寻找领域专家并与其沟通交流的重要性怎么强调都不为过。领域专家有自己的日常工作;他们的时间非常宝贵,因此,明智地利用与他们的交流时间至关重要。
DDD doesn’t work without a domain expert. It’s as simple as that. Without a domain expert, much of the insight and rich domain knowledge and language will not be discovered. Without an expert, the development team may seek advice from users of a current system, and while knowledgeable on the current processes, may not be best placed to provide game-changing wisdom or domain intelligence. It cannot be stressed enough the importance of seeking out a domain expert and engaging with that person. A domain expert will have a day job; her time will be precious, so it is vital that you utilize time with her wisely.
业务分析师并非无能。他们拥有开发人员和领域专家可能不具备的技能。业务分析师可以促进与领域专家的对话,并帮助团队掌握构成 UL 的术语。
Business analysts are not invalid. They hold skills that developers and domain experts may not possess. Business analysts can facilitate conversations with domain experts and can help the team capture terminology that forms the UL.
利益相关者信任领域专家并将其视为专家非常重要。专家理解开展项目的原因及其目标也很重要。与项目性质不符的领域专家可能会成为与其说是帮助,不如说是阻碍。然而,她对项目的担忧和挑战可能是合理的。在这种情况下,确保利益相关者和领域专家进行沟通,以减轻任何恐惧或担忧。
It is important for the stakeholder to trust the domain expert and regard this person as an expert. It is also important for the expert to understand why the project is being undertaken and what its goals are. A domain expert at odds with the nature of the project may turn out to be more of a hindrance than a help. However, her concerns and challenge with the project might be justified. In this case, ensure that the stakeholder and domain expert communicate to allow any fears or worries to be alleviated.
尝试将项目团队与业务部门整合在一起。您的团队应该能够轻松且定期地联系领域专家和用户,以确保持续的反馈。在验证领域模型时,领域专家是您的主要知识来源。尽可能多地从领域专家那里获取信息。通过促进领域专家的知识,您可以解锁更有用的模型。
Try to collocate your project team with the business. Your team should be able to access the domain experts and users easily and regularly to ensure constant feedback. Domain experts are your primary source of knowledge when validating your domain model. Extract as much information from the heads of your domain experts as possible. By facilitating domain experts’ knowledge, you unlock a more useful model.
在解决方案空间中工作时,请确保专注于满足产品的行为,而不是尝试对整个问题域进行建模。通过选择一种行为并定义具体的场景作为示例来推动建模工作。由此,团队和领域专家可以构建一个适合当前问题的模型。这种做法有助于防止过度热衷的开发人员对问题域构建一个“一刀切”的模型,这种模型并非真正根据系统需求量身定制,它更多地反映了现实情况,而不是对现实的有用抽象。
When working in the solution space, ensure that you focus on satisfying the behaviors of the product rather than trying to model the entire problem domain. Drive your modeling endeavors by selecting a behavior and defining concrete scenarios to use as examples. From this, the team and the domain expert can produce a model that is appropriate to the problem at hand. This practice helps to prevent overzealous developers from producing a one-model-to-rule-them-all view of the problem domain that isn’t really tailored to the needs of the system, and is more a reflection of reality rather than a useful abstraction of it.
例如,考虑一下电子商务域名的优惠券功能:
As an example, consider this coupon feature for an e-commerce domain:
要开始为此功能构建模型,您必须根据具体场景进行建模。此功能与多种场景相关。以下是其中一个例子:
To start to shape a model for this feature, you must model to meet specific concrete scenarios. This feature has several scenarios associated with it. The following is one example:
在知识梳理过程中,团队应该倾听领域专家的语言选择,并捕捉用于实现场景的概念。如果团队发现模型存在潜在问题,应该向领域专家提出质疑。
During knowledge crunching, the team should listen to the domain expert’s choice of language and capture concepts that are used to fulfill the scenario. If the team spots potential issues or problems with the model, it should challenge them with the domain expert.
在选择要建模的场景时,不要追求唾手可得的成果;不要忽略简单的数据管理。相反,要着眼于难点——核心领域深处那些有趣的领域。专注于产品的独特之处;这些部分可能比较难,或者可能需要澄清。花在这些方面的时间会很有价值,这正是与领域专家合作如此有效的原因。让领域专家花时间讨论简单的创建、读取、更新和删除 (CRUD) 操作很快就会变得枯燥乏味,领域专家也会很快对你失去兴趣和时间。在产品核心的复杂领域进行建模正是 DDD 原则的初衷。
When picking scenarios to model, don’t go for the low-hanging fruit; ignore the simple management of data. Instead, go for the hard parts—the interesting areas deep within the core domain. Focus on the parts of the product that make it unique; these will be hard or may need clarification. Time spent in this area will be well served, and this is exactly why collaboration with domain experts is so effective. Using domain experts’ time to discuss simple create, read, update, and delete (CRUD) operations will soon become boring, and the domain expert will quickly lose interest and time for you. Modeling in the complicated areas that are at the heart of the product is exactly what the principles of DDD were made for.
作为一名开发人员,对问题领域的模糊性和无知是你最大的敌人。团队必须确保在建模和知识处理过程中,所有术语和语言都定义明确,并且领域专家对这些术语感到满意。每个人都必须达成共识,对概念有统一的理解。除了使用同一种语言与领域专家和其他人交流之外,你还必须使用相同的术语和概念编写代码库,以确保代码模型能够反映对话中的思维模型。
Ambiguity alongside ignorance of your problem domain is your worst enemy as a developer. Teams must ensure that during modeling and knowledge crunching, all terms and language are defined explicitly and the domain expert is happy with the terminology. Everyone must be on the same page and have a single understanding of a concept. Besides talking to domain experts and each other in a single language, you must write the codebase with the same terms and concepts to ensure the code model reflects the mental models in conversation.
UL 是在领域专家和开发团队开始为产品中更重要、更棘手的部分构建解决方案模型时,通过知识碰撞和磨合形成的。开发团队和领域专家之间清晰明确的沟通对于促进探索至关重要,并能降低团队代码模型与领域专家心智模型之间的转换成本。与领域专家讨论设计模式、原则和实践的团队很快就会失去兴趣,因为转换过程需要付出痛苦且高昂的成本。即使是用代码实现的模型,也应该在更高的抽象层次上进行讨论,以便领域专家能够运用其专业知识来解决遇到的每个新场景中的挑战。
The UL is formed from the knowledge crunching exercise between domain experts and the development team as they start to model a solution to the more important and trickier parts of a product. Clear and unambiguous communication between the development team and the domain expert is vital to enable discoveries and to reduce translation cost between the team’s code model and the domain expert’s mental model. Teams that talk to domain experts about design patterns, principles, and practices will soon lose their interest due to the painful and costly translation that is required. The model, even though implemented in code, should be discussed at a higher level of abstraction so that the domain expert can lend his expertise to solving challenges with every new scenario that is thrown at it.
随着您对所从事领域的理解不断加深,您的 UL 也会随之演进。随着语言的发展,您的代码也必须随之演进。重构代码,使用更能揭示意图的方法名称来适应这种演进。如果您发现一组复杂的逻辑开始形成,请与您的领域专家讨论代码的功能,并看看您是否可以为其定义一个领域概念。如果找到了,请将代码的逻辑分组拆分成一个规范或策略类。
As you gain a deeper understanding of the domain you are working in, your UL will evolve. As the language evolves, so must your code. Refactor your code to embrace the evolution by using more intention-revealing method names. If you find a grouping of complex logic starting to form, talk through what the code is doing with your domain expert, and see if you can define a domain concept for it. If you find one, separate the logical grouping of code into a specification or policy class.
当你刚开始一个项目时,你对它知之甚少,但这是你做出重要决策的时候。你的初始模型可能是错误的,但不要太纠结。不断学习问题领域知识的过程需要多次迭代。你的知识会不断增长,并借此将你的模型发展成有用且合适的模型。
When you are starting out on a project, you know little about it, but this is the time when you will be making important decisions. Your initial model will be wrong, but don’t get too hung up. The process of learning more about the problem domain is achieved over many iterations. Your knowledge will grow, and with this you will be able to evolve your model into something useful and appropriate.
当得出第一个有用的模型时,大多数团队通常会停止探索,直接敲键盘实现它。你的第一个模型不太可能是最好的。一旦你有了一个好的模型,你应该把它放下,从不同的方向探索问题。探索和实验对于深入发现和深入了解问题领域至关重要;因此,要勇于犯错,并通过比较好的想法和坏的想法来验证好的想法。
When arriving at the first useful model, most teams usually stop exploring and jump to their keyboards to implement it. Your first model will unlikely be your best. Once you have a good model, you should park it and explore the problem from a different direction. Exploration and experimentation are vital to enable deep discoveries and to learn more about the problem domain; therefore, make mistakes and validate good ideas by comparing them to bad ones.
有时在建模过程中,你会停滞不前;你的解决方案可能让你陷入困境,而新的场景无法用现有模型实现。这没关系。与其试图让场景适应模型,不如创建一个对现有场景和新场景都有用的新模型。尝试忘掉你在第一个模型中积累的所有经验,并朝着新的方向迈进。探索和实验,以发现新的见解并提供新的解决方案。
Sometimes while modeling, you become stagnant; your solution may have painted you into a corner, and a new scenario cannot be fulfilled with the current model. This is fine. Instead of trying to make the scenario fit the model, make a new model that is useful for the existing and new scenarios. Try to unlearn everything you gained for the first model and take a new direction. Explore and experiment to reveal insights and offer new solutions.
从不同角度解决问题的结果并非创建一个完美的模型,而是学习和发现问题领域中的概念。这更有价值,并且能够使团队在每次迭代中都能生成有用的模型。
The result of tackling a problem from various angles is not the creation of a perfect model but instead the learning and discovery of concepts in the problem domain. This is far more valuable and leads to a team able to produce a useful model at each iteration.
一旦您通过与领域专家的交流,为问题领域的复杂子领域推导出模型,就需要用代码来证明它。您和领域专家之间创建的心智模型应该以相同的术语、语言和概念反映在代码中。将心智模型转化为代码后,开发团队可能会发现该模型无法满足场景需求,需要提出新概念或更改现有概念。由于使用了 UL 并且整个团队对模型的理解一致,与领域专家的沟通变得非常容易,新的解决方案可以在协作中得到验证,而无需翻译。代码模型的更新会反映在心智模型中,并且这两个模型会一起发展。
Once you have derived a model for the complex subdomains of your problem domain from sessions with a domain expert, you need to prove it in code. The mental model that was created between you and your domain expert should be reflected in code with the same terminology, language, and concepts. Once it turns the mental model into code, the development team may find that the model does not meet the needs of the scenario, and it needs to make a new concept or change an existing one. Because of the use of the UL and the shared understanding of the model throughout the team, communication with the domain expert is easy, and the new solution can be validated in collaboration and without translation. The update to the code model is reflected in the mental model, and the two models evolve together.
领域模型的创建是一个迭代的过程,在探索实用模型的过程中,它会随着新的业务问题被解决而不断演进。重要的是,不要试图对整个问题领域进行建模,而是选择经过深思熟虑的业务场景,作为测试任何生成的模型的示例。
The creation of a domain model is an iterative exercise, and the quest to discover a useful model will see it constantly evolve as new business problems are tackled with it. It is important not to try to model the whole problem domain but instead select well-thought-through business scenarios that can be used as an example to test any model produced.
领域模型应该始终遵循以下两个原则:
A domain model should constantly adhere to these two principles:
一个实用的模型的创建并非一蹴而就。事实上,领域模型的初始版本往往比较幼稚,对复杂的问题领域缺乏深入的理解。因此,需要不断重构才能在代码库中挖掘出领域知识。
The creation of a useful model is never completed at the first attempt. In fact, often the initial incarnation of a domain model is naive and contains little insight into the rich problem domain. Instead, constant refactoring is required to expose domain knowledge within the codebase.
通过探索和实验,可以发现演进和有效的模型。行为驱动开发 (BDD) 和测试驱动开发 (TDD) 允许您进行实验,并且确保输入和输出不会受到影响。从贫血领域或更简单的模式入手,并在需要时重构为富领域模型。仅当问题需要复杂的解决方案,或者团队对问题领域不确定或不熟悉时(例如,团队从未从事过金融工作),才进行建模。
Evolution and an effective model are discovered through exploration and experimentation. BDD and Test-Driven Development (TDD) allow you to experiment, knowing that the inputs and outputs won’t be affected. Start with an anemic domain or simpler patterns, and refactor toward the rich domain model when needed. Model only when the problem requires a complex solution or the team is unsure of or new to the problem domain (for example, the team has never worked in finance).
保持模型简洁且专注,并力求代码简洁明了。团队常常会很快陷入过度复杂化问题的陷阱。保持解决方案简单并不意味着选择快速而粗暴的方法;而是为了避免混乱和不必要的复杂性。在代码审查或结对编程时,尽量保持简洁。开发人员应该互相挑战,以确保他们提出的解决方案简洁明了,并且该解决方案明确地专注于当前的问题,而不仅仅是针对某个普遍问题的通用解决方案。
Keep your model simple and focused, and strive for boring plain code. Often teams quickly fall into the trap of overcomplicating a problem. Keeping a solution simple does not mean opting for the quick and dirty; it’s about avoiding mess and unnecessary complexity. Use simplicity during code review or pair programming. Developers should challenge each other to ensure they are proving a simple solution and that the solution is explicitly focused only on the problem at hand, not just a general solution to a generalized problem.
如果您正在处理遗留代码库或正在与遗留代码集成,务必确保您的代码不受现有混乱代码的污染。(如果存在混乱,请记住,遗留代码并不意味着糟糕的代码!)清理遗留代码库可能很诱人,但这项任务很快就会变得耗时,并分散您引入新功能的真正目标。相反,您可以依靠防腐层模式在新代码和现有代码之间创建边界。此保护边界使您能够创建一个干净的模型,使其免受其他团队的影响。
If you are working in a legacy codebase or are integrating with a legacy code, it is vital to ensure that your code is not contaminated by the mess that already exists. (If there is mess; remember that legacy doesn’t mean bad code!) It may be tempting to clean up the legacy codebase, but this is a task that can quickly become time consuming and distract from your real goal of introducing new functionality. Instead, lean on the anticorruption layer pattern to create a boundary between your new code and the existing code. This protection boundary enables you to create a clean model that is isolated from other teams’ influences.
在建模过程中,尽早用代码验证和证明模型至关重要。白板绘图和卡片固然很好,但能够运行的代码才是衡量进度的唯一标准。能够运行的代码意味着代码的集成。能够运行的代码连接到数据库和用户界面;它能够从端到端的流程验证模型。
While modeling, it is important to validate and prove the model in code as early as possible. Whiteboard drawings and cards are good, but working code is the only true measure of progress. Working code means integrated code. Working code connects to databases and to user interfaces; it proves the model from an end-to-end process.
经验丰富的开发人员熟悉运用技术重构,通过迁移到众所周知的代码组织模式来提高软件构建的质量。然而,为了确保模型能够清晰地传达其功能,需要不同类型的重构。重要的是,在代码中反映领域专家或任何参与软件产品开发的人员在领域知识方面取得的突破。领域模型中的代码应该清晰且富有表现力。
Experienced developers are familiar with employing technical refactorings to increase the quality of the software construction by migrating to well-known code organizing patterns. However, a different type of refactoring is required to ensure a model communicates what it does clearly. It’s important to reflect in code any domain knowledge breakthroughs that happen with domain experts or indeed anyone working on the software product. Code within the domain model should be clear and expressive.
领域模型最初很简单,基于对领域的浅显理解,通常基于需求文档中的名词和动词,或与领域专家的初步讨论。然而,随着团队知识的深度在迭代过程中不断增长,领域模型的丰富性也应随之提升。即使代码的技术设计已修改以提高清晰度,类的名称、方法以及命名空间也应持续更新,以反映在知识梳理过程中发现的更具洞察力的抽象概念。这种对模型的持续投入有助于保持其相关性,并符合领域专家的愿景和意图。
A domain model starts out simple based on a shallow view of the domain, usually based on the nouns and verbs of the requirement documentation or from initial discussions with domain experts. However, as the depth of knowledge within the team grows through iterations, so should the richness of the domain model. Even if the technical design of the code has been modified to increase clarity, the names and methods of classes, along with the namespaces, should continue to be updated to reflect the more insightful abstractions that are discovered through knowledge-crunching sessions. This continued investment in the model helps keep it relevant and match the vision and intent of the domain experts.
随着模型的增长,您会发现语言中存在歧义或术语过载,或者您可能只是因为模型规模过大而难以管理。为了使大型复杂的领域模型更简单、更易于维护,请根据自然语言和不变量按上下文进行划分。专注于最大限度地降低上下文之间的耦合。不要追求完美的代码;而要追求完美的边界。有界上下文和聚合是领域驱动设计 (DDD) 中强大的概念,可以降低复杂性。这些模式有助于管理复杂性。
As your model grows, you will find ambiguity in language or overloaded terms, or you may just find it difficult to manage due to its size. To make large and complex domain models simpler and easier to maintain, divide by context based on natural language and invariants. Focus on minimizing the coupling between contexts. Don’t strive for perfect code; strive for perfect boundaries. Bounded context and aggregates are powerful concepts in DDD that enable complexity to be reduced. These patterns help to manage complexity.
您应该根据复杂性而非前期设计来制定 DDD 的战略模式。边界一旦定义就很难消除,因此在经过几次开发迭代,并且您对问题领域有了更深入的了解之后,再根据边界进行重构。优先考虑小型代码模块,并关注可替换的边界,而不是追求边界内的完美。隔离并保护数据。
You should arrive at the strategic patterns of DDD due to complexity rather than up-front design. Boundaries are hard to remove when they are defined so refactor toward them after several iterations of development and when you are more knowledgeable within the problem domain. Favor small modules of code with a focus on boundaries that you can replace rather than perfection within those boundaries. Isolate and protect data.
图 10.1直观地展示了应用 DDD 原则和实践的步骤。
Figure 10.1 visualizes the steps of applying the principles and practices of DDD.
图 10.1 DDD 的过程。
FIGURE 10.1 The process of DDD.
模型在不断发展变化;如果不拥抱演进,就无法有效地利用 DDD 的实践和模式。您应该持续执行本节介绍的步骤。尽可能降低软件解决方案的复杂度是 DDD 的目标。所有原则和实践都是为了实现这一总体目标。作为开发人员,您的工作是随着迭代的变化不断挑战模型设计的有效性和简洁性,并不断改进模型设计以满足利益相关者的新行为。
A model is constantly evolving and changing; you cannot effectively utilize the practices and patterns of DDD without embracing evolution. You should perform the steps presented in this section constantly. Keeping the complexity of the software solution as low as possible is the goal of DDD. All the principles and practices are aimed at this overall goal. As a developer, it is your job to continuously challenge the effectiveness and simplicity of your model design as iterations change and evolve it to meet the new behaviors of the stakeholders.
一个有用的模型是通过数百次小的重构而形成的。模型不断地进行调整,通过小的变换来解锁新的发现。
A useful model is arrived at through hundreds of small refactorings. Adjustments to the model are made constantly, with discoveries being unlocked through small transformations.
在开发过程中,你可能会发现你对核心领域的假设会发生变化。业务部门可能会发现它错了。情况瞬息万变。当你意识到新的不变量会使你的设计失效时,你的边界也会随之改变。随着新功能的推出,你可能会发现歧义逐渐出现。如果出现这种情况,请拆分模型,并在特定上下文中明确定义这两个部分。
During development, you may find that your assumption on the core domain may change. The business may discover that it was wrong. Things change. Your boundaries will also change as you realize that new invariants invalidate your design. With new features, you may find that ambiguity creeps in. If it does, split the model and explicitly define the two parts in specific contexts.
记住,先从简单的开始,等真正需要的时候再逐步应用原则和实践。否则,你很可能会把一个简单的解决方案复杂化,从而解决一个简单的问题。
Remember to start simple, and move toward the principles and practices when you absolutely need them. Otherwise, you may very well overcomplicate a simple solution to a simple problem.
丰富实用的模型是探索和创造力的产物。实验就是从不同的角度审视代码。如果你发现编码很难,那么你可能做错了什么。不要仅仅停留在第一个有用的模型上。由于模型需要经过多次迭代才能发展,因此最好推迟重构,直到你对领域有了足够的了解。让模型活下去,不断演变。你不可能第一次就成功。实验和探索能够促进学习。
A rich and useful model is a product of exploration and creativity. Experimentation is about looking at the code in a different way. If you find that coding is hard, you are probably doing something wrong. Don’t just stop at the first useful model you produce. Because a model evolves over a number of iterations, it is a good idea to delay refactoring until you know enough about the domain. Let the model live a little and evolve. You won’t get it right the first time. Experimentation and exploration fuel learning.
在每次迭代中,开发团队都必须挑战其假设,因为新增的功能可能意味着之前有用的模型不再适用。这项技能可以使您的软件保持灵活性,并随着产品的演进而发展,从而防止其变成“大泥球”(BBoM)。
During every iteration, the development team must challenge its assumptions because additional features may mean that a previously useful model is no longer fit for purpose. This skill enables your software to be flexible and evolve as the product evolves, thus preventing it from turning into a Big Ball of Mud (BBoM).
团队不应过于执着于基于第一次迭代需求的模型。后续迭代可能会发现模型无法满足新的功能需求。缺乏灵活性的团队很快就会发现代码对他们不利。当团队遵循测试驱动开发方法时,他们不会在系统正常运行时就止步不前。相反,他们会先通过测试,然后重构设计,使其更具表现力。这是测试驱动开发迄今为止最重要的方面,也应该应用于领域驱动设计 (DDD)。在获得知识后进行重构有助于生成更具表现力和启发性的模型。
A team should not become too attached to a model based on the requirements from a first iteration. Subsequent iterations may see the model become inadequate for the new feature requests. Teams that are inflexible about evolving will soon find that the code works against them. When teams follow a test-driven development methodology, they do not stop when the system is working. Instead, they make a test pass and then refactor their design to make it more expressive. This is by far the most important aspect of test-driven development and one that should be applied to DDD. Refactoring when knowledge is gained helps to produce a model that is more expressive and revealing.
建模活动可以在您需要时随时进行;它不是项目方法论中的一个步骤。您可以随时抽身与领域专家合作。如果您对所从事的领域有深入的了解,您可能会发现根本不需要建模。不要过于依赖您的软件;当业务模型发生变化时,要做好丢弃最佳代码的准备。每次迭代都会带来新的挑战。您需要确保不断改进和重塑您的模型,以满足新功能和新场景的需求。
The activity of modeling happens whenever you need it; it is not a step in a project methodology. You break out and collaborate with domain experts whenever it is required. If the area you are working on is well understood, you may find that you don’t need to model at all. Don’t get too attached to your software; be prepared to throw away your best code when the business model changes. With each iteration comes a new challenge. You need to ensure that you refine and reshape your model to meet the needs of new features and scenarios.
不存在愚蠢的问题或愚蠢的想法。错误的模型有助于验证有用的模型,而创建模型的过程有助于学习。在复杂或核心的领域工作时产品,团队必须准备好以不同的方式看待事物,承担风险,并且不害怕将问题颠倒过来。
There is no such thing as a stupid question or a stupid idea. Wrong models help to validate useful ones, and the process of creating them aids learning. When working in a complex or core area of a product, teams must be prepared to look at things in a different way, take risks, and not be afraid of turning problems on their head.
对于复杂的核心领域,团队应该至少构建三个模型,这样才有机会产出一些有用的东西。那些失败次数不够多、想法也不够多的团队,很可能是因为他们还不够努力。在与领域专家深入交流时,团队不应该因为一有有用的想法就停止头脑风暴。一旦团队找到合适的思路,就应该擦掉白板,从不同的角度重新开始,尝试假设分析法。当团队与专家达到最佳状态时,应该一直保持这种状态,直到用尽所有想法。
For a complex core domain, a team should produce at least three models to give itself a chance at producing something useful. Teams that are not failing often enough and that are not producing many ideas are probably not trying hard enough. When deep in conversation with a domain expert, a team should not stop brainstorming at the first sign of something useful. Once the team gets to a good place, it should wipe the whiteboard and start again from a different angle and try the what-if route of investigation. When a team is in the zone with an expert, it should stay there until it exhausts all its ideas.
在本书的第二、三和四部分中,您将学习组织代码库的模式,使其能够更有效地适应新需求。这些灵活的代码源于小型重构的迭代。不断重构以获得更深入的洞察,有助于实现灵活的设计和代码,从而能够促进变更并适应系统在每次迭代中添加的新功能。如果模型不灵活,它就毫无用处。Martin Fowler 在他的《分析模式:可重用对象模型》一书中提出了一个重要的建模原则: “设计模型时,应确保最频繁的模型修改只会导致最少类型的更改。”
In Parts II, III, and IV of this book, you will learn about patterns to organize your codebase to enable it to change more effectively with new requirements. This supple code is derived from iterations of small refactors. Constantly refactoring toward deeper insight helps lead to a supple design and flexible code that is able to facilitate change and adapt to new features of the system as they are added in each iteration. If a model is not supple, it is not useful. Martin Fowler states an important modeling principle in his book Analysis Patterns: Reusable Object Models: “Design a model so that the most frequent modification of the model causes changes to the least number of types.”
但是,要小心不要过早重构。在对领域有足够了解之前,不要进行重构,也不要专注于应用设计模式。推迟重构还可以揭示代码中哪些部分更改最频繁以及原因。有了这些了解,您就可以对代码库进行更明智的设计更改。
However, be careful of premature refactoring. Don’t refactor until you know enough about the domain, and don’t become preoccupied with applying design patterns. Delaying refactoring can also reveal which areas of the code change most often and why. With this knowledge, you can make more informed design changes to your codebase.
当团队深入代码库时,他们常常会忽略或忽略逻辑语句,将其视为简单的编程产物。这些隐式的小代码块隐藏了领域相关的重要细节,往往掩盖了它们的重要性。如果这些设计决策没有明确说明,就无法将其添加到心智模型中,进一步的设计探索也会变得更加困难。
When teams are working deep within a codebase, they often ignore or dismiss logic statements as simple artifacts of programming. These small implicit code blocks hide important details about the domain, often disguising their importance. If these design decisions are not made explicit, they cannot be added to the mental model, and further design discoveries will be harder.
如前所述,延迟重构可能会暴露代码中的重要细节,从而暴露模型中的重要细节。如果您发现某个代码分组代表某种没有明确名称的领域逻辑,请告知领域专家,命名该逻辑概念,并将代码包装在该概念中。将隐式概念明确化至关重要。您在代码中做出的任何决策都需要明确反馈给领域专家,并将其作为模型的概念进行捕获。
As mentioned previously, delaying refactoring can reveal important details in the code and thus important details of the model. If you find a code grouping that represents some kind of domain logic that doesn’t have an explicit name, inform the domain expert, name the logic concept, and wrap the code in the concept. It is vital to make implicit concepts explicit. Any decisions you make in code need to be explicitly fed back to the domain expert and captured as a concept of the model.
领域专家未提及或仅略微提及的内容往往是解锁模型中深层发现的关键。这些对领域专家来说可能并不重要的隐性概念应该明确化、命名化,并让开发团队充分理解。例如,假设一个电商网站禁止海外客户在购物车中添加超过 50 件商品。开发人员可以轻松实现此业务规则,如清单 10-1 所示。
It’s often the things the domain experts don’t say or barely hint at that are the key to unlocking deep discoveries within a model. These implicit concepts that may not appear important to domain experts should be made explicit, be named, and be fully understood by the development team. For example, consider an e-commerce site that prevents overseas customers from adding more than 50 of any one item to their basket. Developers can easily implement this business rule, as can be seen in Listing 10-1.
然而,在未来的几个月里,其他开发人员可能无法理解为什么存在这样的规则。相反,您应该理解规则存在的原因,并相应地命名代码部分。事实上,供应商强制执行这样的规则是为了防止网站充当批发商。有了这些知识,您可以将代码包装在一个类中,以表明对领域有更深入的了解和理解,从而使代码明确地揭示这条规则。此重构如清单 10-2 所示。
However, in future months, other developers may not understand why such a rule exists. Instead, you should understand why the rule exists and name the portion of code accordingly. As it transpires, suppliers enforce such a rule to prevent sites acting as wholesalers. With this knowledge, you can make the code explicitly reveal this rule by wrapping it in a class that indicates a deeper insight and understanding of the domain. This refactoring is seen in Listing 10-2.
开发人员应注意代码或领域专家语言中的歧义或不一致之处。在讨论领域模型时,还应特别注意其他团队成员的语言。务必通过与领域专家沟通,验证关于语言和模型细节的假设。大声验证,并确保语言和设计决策的一致性。如果领域专家没有提及,则不应出现在语言或代码库中。如果模型中的某个术语不再有意义或无用,请将其删除。保持语言简洁明了。领域专家应该对语言或模型中不恰当的术语或结构提出异议。
Developers should watch for ambiguity or inconsistency in code or in the language of a domain expert. You should also ensure that you pay particular attention to the language of other team members when discussing the domain model. Always validate assumptions about the language and details of the model by talking to domain experts. Validate aloud, and confirm your language and design decisions with linguistic consistency. If a domain expert doesn’t say it, it shouldn’t be in the language or codebase. If a term in the model no longer makes sense or is not useful, remove it. Keep your language small and focused. Domain experts should object to inadequate terms or structure in the language or model.
如果领域专家谈论某个概念,请明确说明。如果领域专家暗示了某个概念,请明确说明。如果您谈论的某个概念令领域专家感到困惑,则可能是您误解了他们所说的某些内容,您需要完善您的 UL。给它起个名字,如果想不出一个好名字,请将其推迟,并称之为蓝色政策,直到您能想到更有意义的名字。
If domain experts talk about it, make it explicit. If domain experts hint at a concept, make it explicit. If you talk about something that puzzles domain experts, maybe you have misunderstood something they have said and you need to work on your UL. Give things a name, and if you can’t think of a good name, defer it and call it the blue policy until you can think of something more meaningful.
领域模型应该传达业务意图。确保仔细命名类的所有方法和属性。尝试通过以下方式描述行为:不要把你的代码设计留给别人去解读。编写富有洞察力的代码,展现领域语言的丰富内涵,不仅能帮助你自己,也能帮助其他开发者。
A domain model should communicate the intent of the business. Ensure that you take care in naming all methods and properties of your classes. Try to describe the behaviors by involving the UL. Don’t leave your code design up to interpretation. Help yourself and other developers by writing code that is insightful, revealing the rich language of the domain.
软件开发人员主要是一位问题解决者,他们利用技术来实现解决方案。开发人员非常擅长学习技术和项目方法;然而,分解问题并提炼重要信息和次要信息的能力,将使一名优秀的开发人员成为更优秀的开发人员。你应该在问题领域投入与解决方案领域同等多的时间。
A software developer is primarily a problem solver who utilizes technology to implement a solution. Developers are fantastic at educating themselves on technology and project methodologies; however, decomposing a problem and being able to distill what is important from what is not will enable a good developer to become a great one. You should spend as much time in the problem space as you do in the solution space.
正如一个有用的模型需要经过一系列迭代才能得出一样,问题空间也必须经过细化才能揭示利益相关者愿景背后的真实意图。倾听利益相关者的意见,并了解他们为什么、做什么、何时做,是开发人员应该练习的一项技能,就像他们练习编码技巧一样。
Just as a useful model is derived over a series of iterations, so too must a problem space be refined to reveal the true intent behind a stakeholder’s vision. Listening and drawing the why as well as the what and when from stakeholders is a skill that developers should practice just as they practice coding katas.
代码是 DDD 的产物,而不是过程;您无需技术解决方案即可解决问题。
Code is a product of DDD, not the process; you can solve problems without having a technical solution.
所有问题都各有不同;有些问题很复杂,而且业务价值不大,因此没有必要浪费精力去寻找自动化解决方案。复杂的边缘情况并不总是需要自动化解决方案。人类可以通过例外情况进行管理。如果一个问题很复杂,并且构成了边缘情况,请与您的利益相关者和领域专家讨论自动化的价值。您的精力可以更好地投入到其他方面,而人类可能更适合处理这种例外情况。
All problems are not created equal; some are complex and are of little business value, so it makes no sense to waste effort in finding automated solutions for them. Complex edge cases do not always need automated solutions. Humans can manage by exception. If a problem is complex and forms an edge case, speak to your stakeholder and domain expert about the value of automating it. Your effort could be better served elsewhere, and a human might better handle this exception.
与成为 Scrum Master 不同,应用 DDD 的原则和实践不会获得任何证书。您的投资回报将来自一个易于理解、易于维护、满足利益相关者期望、并且充满乐趣的产品。
Unlike becoming a scrum master, there is no certificate awarded when applying the principles and practices of DDD. Your reward from your investment will result in a product that is easily understood, straightforward to maintain, meets the expectations of your stakeholders, and is fun to work on.
你还会发现团队成员对业务的理解更加深入。他们会更加流畅地与利益相关者沟通,并为企业之前未意识到或尚未解决的问题提供解决方案。
You will also find that your team members understand the business better. You will notice that they will be able to talk more fluently with stakeholders and offer solutions to problems that the business didn’t know it had or maybe did not have a solution for.
团队与业务的协调一致,确保组织中的每个人都理解价值的含义。团队将不再花费时间去构建过于复杂、技术性强、受人影响的解决方案,这些解决方案采用相同的架构和工作量,即使是对业务不太重要的领域,也需要追求代码的完美。相反,他们将能够分解问题,与业务部门合作,专注于价值,并在此领域投入时间,为任何支持性或通用领域提供足够好、简单的解决方案。他们将理解真正的价值所在,以及他们能够在哪些方面有所作为。
Aligning a team and a business ensures everyone in the organization understands what value means. Teams will no longer spend time on overcomplicated, technical, influenced solutions that use the same architecture and effort, striving for code perfection even for areas that are of little importance to the business. They will instead be able to decompose problems and work with the business to focus on value and spend time in this area, proving good enough, simple solutions to any supporting or generic domains. They will understand where the true value is and where they can make a difference.
团队将专注于问题领域,理解它,而不是仅仅关注技术解决方案。他们将花更多时间思考问题是什么、为什么以及何时发生,而将如何解决的问题留到以后再说。
Teams will focus on the problem domain, understanding it rather than focusing only on the technical solution. They will spend increased time on the what, why, and when, leaving the how to later.
秉持 DDD 理念的团队更注重全局,并了解在何处投入最大精力。他们不会将相同的架构应用于解决方案的所有部分,也不会在价值不大的领域追求完美。他们会放弃独立且可运行的软件,转而追求不必要的优雅和华丽的装饰。
Teams that are aligned with the philosophy of DDD focus more on the bigger picture and understand where to put the most effort. They will not apply the same architecture to all parts of a solution, and they will not strive for perfection in areas of little value. They will trade isolated and working software for unnecessary elegance and gold plating.
只有核心领域由于其复杂性或重要性才需要优雅。这并不是说所有其他代码都应该写得很糟糕,而是应该被隔离,用边界定义,并暴露行为来支持核心领域。
Only the core domains need to be elegant due to complexity or importance. This is not to say that all other code should be poorly written, but it should be isolated, defined by a boundary, and expose behavior to support the core domain.
软件开发是一个学习的过程,DDD 也是如此。如果你想擅长任何事情,你都需要练习、练习、再练习。如果你想成为一名优秀的开发人员而不是一名普通的开发人员,你需要对问题和解决方案表现出热情。要应用 DDD 的原则,你需要一个有动力且全心投入的团队——一个致力于学习其工艺和所针对的问题领域的团队。热情存在于我们每个人心中,如果你觉得 DDD 的实践很有价值,那么你就应该激励你的团队,成为一名传播者。热情是会传染的;如果你承诺花时间与领域专家一起更深入地理解某个领域,并能展示如何通过这种方式构建更具表现力的代码库,那么你的团队就会效仿。
Software development is a learning process, and so is DDD. If you want to be good at anything, you need to practice, practice, practice. If you want to be a great developer rather than a good one, you need to show passion for the problem and passion for the solution. To apply the principles of DDD, you need a driven and committed team—a team committed to learning about its craft and the problem domains it works in. Passion lies within all of us, and if you feel value in the practices of DDD, it is up to you to inspire your team and become an evangelist. Passion is contagious; if you commit to spend time with your domain experts to understand a domain at a deeper level and can show how this results in a more expressive codebase then your team will follow.
许多开发人员在棕地环境中工作时会感到厌烦,因为他们害怕不得不在其他开发人员的代码库上工作。在企业系统上工作时,你有时必须集成或在棕地环境中工作。优秀的开发人员擅长以安全且可维护的方式将新功能引入现有代码库。
Many developers are turned off when working in a brownfield environment because of fear of having to work on another developer’s codebase. When working on enterprise systems, you have to integrate or work on brownfield environments at some point. Great developers excel at introducing new features into an existing codebase in a safe and maintainable manner.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 11 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 11 download and individually named according to the names throughout the chapter.
在确定系统中的有界上下文后(如第 6 章“使用有界上下文维护领域模型的完整性”和第 7 章“上下文映射”中所述),下一步是决定如何集成它们以执行完整的业务用例。在此过程中,您面临的一大挑战是成功设计一个健壮的分布式系统。例如,下订单、向客户开具账单和安排发货的每个步骤可能属于不同的有界上下文,这些上下文作为单独的软件在单独的物理机器或云实例上运行。在本章中,您将了解分布式计算中的基本概念,这些概念使您能够保留明确的领域概念,同时优雅地处理分布式系统固有的非功能性需求,例如可伸缩性和可靠性。
After identifying the bounded contexts in your system (as discussed in Chapter 6, “Maintaining the Integrity of Domain Models with Bounded Contexts,” and Chapter 7, “Context Mapping”), the next step is to decide how you will integrate them to carry out full business use cases. One of the big challenges you face in this process is successfully designing a robust distributed system. For example, each step of placing an order, billing the customer, and arranging shipping may belong to a different bounded context running as a separate piece of software on a separate physical machine or cloud instance. In this chapter, you learn about fundamental concepts in distributed computing that allow you to retain explicit domain concepts while gracefully dealing with nonfunctional requirements, such as scalability and reliability, that are inherent to distributed systems.
技术挑战只是集成有界上下文和构建分布式系统的一部分,社交挑战则是另一部分。分布式系统通常过于庞大,单个团队难以承担,需要多个团队共同负责一个或多个有界上下文。本章将向您介绍成功的团队运用领域驱动设计 (DDD) 构建高度可扩展系统的团队协作和沟通模式。其中一种常见的模式是采用面向服务的架构 (SOA)。
Technical challenges are only one part of integrating bounded contexts and building distributed systems; social challenges are the other. Distributed systems are often too large for a single team to maintain responsibility of, requiring a number of teams to take ownership of one or more bounded contexts. This chapter introduces you to teamwork and communication patterns that successful teams use to build highly scalable systems using Domain-Driven Design (DDD). One common pattern is the adoption of Service Oriented Architecture (SOA).
SOA 是一种用于构建面向业务、松散耦合软件服务的架构风格。本章将向您展示,通过将有界上下文概念化为 SOA 服务,您可以利用 SOA 的原则创建松散耦合的有界上下文,从而帮助解决有界上下文集成的技术和社会挑战。您还将了解事件驱动的响应式编程带来的优势,以及它如何通过将有界上下文之间的通信建模为领域中发生的事件来与 DDD 协同工作。
SOA is an architectural style for building business-oriented, loosely coupled software services. This chapter shows you that, by conceptualizing bounded contexts as SOA services, you can use the principles of SOA to create loosely coupled, bounded contexts that help solve the technical and social challenges of bounded context integration. You also learn about the benefits event-driven reactive programming provides and how it synergizes with DDD by modeling communication between bounded contexts as events that occur in the domain.
事件驱动系统也带来了挑战。最显著的是,它们要求开发人员以不同的方式思考系统设计,并最终实现一致性。因此,本章还讨论了这些挑战的缺点和应对方案。此外,本章还涉及一些运维方面的问题,例如监控服务级别协议 (SLA) 和错误。
Event-driven systems also bring challenges. Most notably they require developers to think differently about how they design their systems, and also give rise to eventual consistency. So this chapter also discusses the drawbacks and options for dealing with them. In addition this chapter also touches on operational concerns like monitoring service level agreements (SLAs) and errors.
阅读完本章(该章奠定了有界上下文集成的基础)后,接下来的章节将提供具体的代码示例,帮助您应用这些概念构建集成有界上下文的系统。完成本章和接下来的两章后,您将能够开始运用这些概念和新的技术技能,与 DDD 协同构建事件驱动的分布式系统。
After reading this chapter that lays the foundation of bounded context integration, the next chapters provide concrete coding examples of building systems that integrate bounded contexts by applying these concepts. After completing this and the next two chapters, you will be ready to start applying these concepts and your new technical skills to build event-driven distributed systems synergistically with DDD.
软件服务之间需要建立关系才能提供高级行为。您有责任选择这些关系和沟通方式。这项艰巨的责任会对交付速度、效率和项目成功产生重大影响。您可能需要与外部支付提供商集成,或者可能需要与公司其他团队编写的系统进行通信。事实上,在大多数项目中,您可能都存在许多类似的内部和外部关系。
Software services need to have relationships with each other to provide advanced behaviors. It is your responsibility to choose these relationships and the methods of communication. This massive responsibility can have significant impacts on the speed of delivery, the efficiency, and the success of a project. You may need to integrate with an external payment provider, or you may need to communicate with a system written by another team in your company. In fact, you probably have a number of internal and external relationships like this on most projects.
当您选择能够反映领域特征的服务间关系时,您将获得 DDD 的诸多优势,例如,清晰的模型有助于与领域专家进行沟通,从而顺利地融入新概念。许多组织发现,合理选择边界和通信协议,可以让每个团队独立工作,而不会妨碍其他团队。
When you choose relationships between services that reflect your domain, you get the familiar benefits of DDD, such as an explicit model that facilitates conversations with domain experts, allowing new concepts to be incorporated smoothly. Many organizations find that judicious choice of boundaries and communication protocols allows each team to work independently without hindering others.
仅仅是沟通方式的选择,就足以决定一个应用程序能否在病毒式增长的时期扩展到千万用户,还是会在同样的场景下崩溃,拖垮整个业务。一旦确定了关系,选择沟通方式通常会更容易。识别关系的一个好方法是识别你的有界上下文。
The choice of communication method alone can be the difference between having an application that scales up to ten million users during periods of heavy viral growth, and a system that collapses in the same scenario taking the whole business down with it. Choosing the communication method is often easier once you’ve identified the relationship. A good place to start identifying relationships is by identifying your bounded contexts.
随着系统规模的扩大,依赖关系会变得越来越重要,这会带来负面影响。除非有充分的理由,否则您应该尽量避免大多数形式的耦合。代码耦合意味着一个团队可能会破坏另一个团队的代码,或者造成瓶颈,从而减慢新功能的交付速度。子系统之间的运行时耦合意味着一个系统如果没有另一个系统就无法运行。
As systems grow, dependencies become more significant in a negative way. You should strive to avoid most forms of coupling unless you have a very good reason. A coupling on code means that one team can break another team’s code or cause bottlenecks that slow down the delivery of new features. A runtime coupling between subsystems means that one system cannot function without the other.
如果设计松耦合的有界上下文,并限制依赖关系,那么每个有界上下文都可以独立开发。它的代码库可以进行演进,而不必担心破坏另一个有界上下文的行为,并且它的开发人员也不必等待其他有界上下文的开发人员完成工作或批准更改。
If you design loosely coupled bounded contexts that limit dependencies, each bounded context can be developed in isolation. Its codebase can be evolved without fear of breaking behavior in another bounded context, and its developers do not have to wait for developers in other bounded contexts to carry out some work or approve a change.
最终,当有界上下文松散耦合时,瓶颈可能会更少,并且更快、更有效地创造商业价值的可能性更高。
Ultimately, when bounded contexts are loosely coupled there are likely to be fewer bottlenecks and a higher probability that business value will get created faster and more efficiently.
您在第六章中了解到,有界上下文代表离散的业务功能,例如销售或运输。正如您在公司走廊里走动时会看到标识各个业务部分的标志一样,根据这些业务功能对软件系统进行划分是一种很好的做法。
You learned in Chapter 6 that bounded contexts represent discrete business capabilities, like sales or shipping. Just as you might walk through the corridors of the company you work for and see signs identifying each part of the business, it is good practice to partition your software systems in line with these business capabilities.
当你查看运输代码时,你关注的是业务的运输部分。如果销售部门的概念妨碍了你添加集成新运输供应商的新功能,那么这样做就没什么用了。事实上,修改运输代码可能会破坏销售功能。如果你听说过单一职责原则 (SRP),你就会明白这一点。在领域驱动设计 (DDD) 中,你可以使用 SRP 将独立的业务功能隔离到单独的有界上下文中。不过,有时将有界上下文作为同一解决方案中的独立模块/项目存在也是可以接受的。
When you’re looking at the shipping code, you’re focused on the shipping part of the business. It’s not that helpful if you have concepts from the Sales Department getting in the way of adding a new feature that integrates a new shipping provider. In fact, making changes to the shipping code might break sales features. If you’ve heard of the Single Responsibility Principle (SRP), this will make perfect sense to you. In DDD, you can use the SRP to isolate separate business capabilities into separate bounded contexts. It is sometimes acceptable for your bounded contexts to live as separate modules/projects inside the same solution, though.
确定了有界上下文后,提醒所有构建系统的人,有一个更宏观的视角会很有帮助。将所有有界上下文放在一个代码仓库或解决方案中,可以帮助开发人员看到他们所处的有界上下文之外的世界。理解这个宏观视角非常重要,因为有界上下文结合起来才能实现完整的业务用例。
Once you have identified bounded contexts, it’s useful to remind everyone building the system that there is a bigger picture. By putting all of the bounded contexts inside a single code repository or solution it can help developers to see that there is a world outside of their bounded context. Understanding the bigger picture is important because bounded contexts combine to carry out full business use cases.
不过,每个有界上下文并非必须存在于同一个源代码库或解决方案(例如,同一个 Visual Studio 解决方案)中。在某些情况下,这是不可能的,因为有界上下文是用不同的编程语言编写的,甚至不在同一个操作系统上运行。
It is not strictly necessary for each bounded context to live inside the same source code repository or solution (e.g., a Visual Studio solution), though. In some cases, it is not possible because bounded contexts are written in different programming languages that do not even run on the same operating system.
没有单一的正确答案可以决定你的有界上下文代码应该放在哪里。你需要评估各种利弊,并决定哪种方法最适合你。
There is no single correct answer that determines where the code for your bounded contexts should live. You need to assess the trade-offs and decide which approach is best for you.
如果您选择将所有有界上下文都放在一个解决方案中,那么它们之间建立依赖关系的风险就会增加。如果两个有界上下文都使用了同一解决方案中另一个项目的代码,那么其中一个有界上下文可能会破坏另一个有界上下文,从而带来危险的风险。假设您在一个名为Eccommerce.Common或类似名称的共享项目中有一个名为User 的类:
If you do choose to locate all of your bounded contexts inside a single solution, there is an increased risk of creating dependencies between them. If two bounded contexts use code from another project inside the same solution, there is a dangerous risk of one bounded context breaking the other. Imagine that you have a User class in a shared project called Eccommerce.Common or similar:
public class User
{
public String Name {get; set;}
public String Id {get; set;}
public void UpdateAddress(Address newAddress)
{
...
}
}
public class User
{
public String Name {get; set;}
public String Id {get; set;}
public void UpdateAddress(Address newAddress)
{
...
}
}
如果运输限界上下文决定更改UpdateAddress()的实现,它可能会破坏销售限界上下文,因为销售限界上下文依赖于将旧地址持久化到特定位置或格式。此外,依赖共享代码的团队可能需要召开会议,商定如何更改以及何时能够更新代码以适应更改。团队之间的这种依赖关系可能会减慢项目进度,并引发不良的政治冲突。图 11.1可视化了一个包含多个独立限界上下文的解决方案。
If the Shipping bounded context decides to change the implementation of UpdateAddress(), it might break the Sales bounded context, which relied on the old address being persisted in a certain location or format. Also, the teams that rely on the shared code might need to have meetings to decide how it can be changed and when they will be able to update their code to accommodate the change. This kind of dependency between teams can slow down a project and introduce undesirable political scenarios. Figure 11.1 visualizes a single solution that contains multiple independent bounded contexts.
图 11.1单个解决方案内的多个有界上下文。
FIGURE 11.1 Multiple bounded contexts inside a single solution.
另一个拖慢团队效率的常见依赖关系是数据库。例如,销售限界上下文的团队想要更新用户架构,但没有人确定这是否会破坏发货限界上下文或计费限界上下文中的代码。这可能会导致多个团队分心,无法处理业务优先级,而转而支持销售限界上下文的架构变更。团队之间的依赖关系并不理想,因为在团队同步时,新功能的交付速度会降低。
Another common dependency that slows down teams is the database. For example, the Sales bounded context’s team wants to update the User schema, but no one is sure if this will break code in the Shipping bounded context or the Billing bounded context. It’s likely that several teams will be distracted from working on business priorities to support the Sales bounded context’s schema change. A dependency between teams is undesirable because the rate of delivering new features is reduced while the teams synchronize.
如果您以前使用过数据库集成,您可能还会熟悉另一个常见问题:通过数据库集成的每个模型都有相似但又不同的领域概念。当多个模型使用相同的共享模式时,这种策略会变得很麻烦。例如,在家具领域,供应商可能销售大批量生产的家具和定制家具。这两个业务部分可能存在许多差异,并且可能是单独的限界上下文。但如果它们位于同一个代码库中,则每个模型的相似性很可能会导致所有类型的家具使用共享模式,如图11.2所示。
If you’ve used database integration in the past you may also be familiar with another common problem, where each model that integrates through the database has similar but distinct domain concepts. This strategy becomes painful when multiple models use the same shared schema. For example, in the domain of furniture, suppliers may sell mass-produced furniture and custom-made furniture. These two parts of the business are likely to have many differences and are likely to be separate bounded contexts. But if they reside in the same codebase, there is the likelihood that the similarities of each model will result in the use of a shared schema for all types of furniture, as shown in Figure 11.2.
图 11.2使用共享模式的多个有界上下文。
FIGURE 11.2 Multiple bounded contexts using a shared schema.
最初,定制和量产限界上下文这两个模型可能很容易映射到共享模式上,但它们将来可能会出现分歧。当模型之间开始出现差异时,共享模式可能会包含与一个模型相关而与另一个模型无关的列。例如,定制家具模型可能需要一个新的制造优先级列,该列与量产限界上下文无关。然而,共享模式需要包含这个新字段。在某些情况下,每个模型甚至可能将列用于不同的目的。从多个限界上下文使用的共享模式创建报告可能容易出错、产生歧义或误导业务。
Initially, the two models of the custom-made and mass-produced bounded contexts may easily map onto the shared schema—but they are likely to diverge in the future. When differences between the models start to appear, the shared schema may have columns relevant to one model but not the other. For example, the custom-made furniture model may require a new manufacturing _ priority column that has no relevance to the mass-produced context. Yet the shared schema will need to include the new field. In some scenarios columns may even be used for different purposes by each model. Creating reports from a shared schema that is used by multiple bounded contexts can be error-prone, ambiguous, or misleading to the business.
从根本上讲,在不同语义的模型之间共享模式可能会违反 SRP,代价高昂。当代码库较小时,后果可能不那么严重。但随着系统的增长,由于每个有界上下文都指向不同的方向,这种痛苦可能会呈指数级增长。
Fundamentally, sharing a schema between models with different semantics can be an expensive violation of the SRP. When the codebase is small the consequences are less severe. But as the system grows, the pain is likely to increase exponentially as each bounded context pulls in different directions.
如果将业务领域拆分成多个有界上下文,每个上下文都有自己的代码库,就能避免一大堆组织问题。当所有开发人员都在一个大型代码库上工作时,单个代码库上的在制品 (WIP) 数量会比拥有多个小型代码库(每个代码库都有少量 WIP)时更多。过多的 WIP 是软件行业效率低下的一个主要原因,而且通常容易被忽视。
If you split your domain into multiple bounded contexts that each has its own codebase you will preclude a whole category of organizational problems. When all of your developers are working on a single large codebase you will have more work in progress (WIP) on a single codebase than if you had a greater number of smaller codebases (each with a small amount of WIP). Excess WIP is a prolific, and usually unnoticed, source of inefficiency in the software industry.
当你想要发布新功能时,WIP 会成为一个问题。如果其他功能仍在开发中,如何发布一个已完成的功能呢?许多团队会选择功能分支;当一项工作完成后,它会被合并到主分支并发布。但这意味着你拥有的是长期存在的分支。如果你在一个分支上工作了两周后才尝试合并,你可能会落后太多,甚至无法完成合并。本质上,你会失去持续集成的所有好处。而且,你在编写代码上花费的时间与你在处理合并和发布代码上花费的时间一样多。
WIP is a problem when you want to release new features. How can you release one completed feature if other features are still in progress? Many teams turn to feature branches; when a piece of work is complete, it is then merged into the master branch and released. But this means that you have long-lived branches. If you try to merge a branch after working on it for two weeks, you may be so far behind that the merge may not even be possible. Essentially you lose all the benefits of continuous integration. And you can spend as much time fighting merges and releasing code as you spend on writing it.
在一个大型领域中,你很容易就会有 10 名或更多的开发人员分别开发不同的功能。他们会在添加新功能方面取得巨大进展,但正如前文所述,尝试合并和发布代码可能会非常痛苦。因此,部署可能会更加危险,需要更多的 QA 和手动回归测试。在最糟糕的情况下,部署可能会耗费公司一整天的时间,而最终却没有创造任何新的价值。
In a large domain you can easily have 10 or more developers all working on separate features. They will make great progress in adding the new features, but as-mentioned, trying to merge and release the code can be excruciatingly painful. Consequently deploys are likely to be more risky, needing more QA and manual regression testing. In the worst case, deploys can take a whole day of the company’s time where no new value is created.
如今,许多公司都在使用持续交付,每天多次向客户部署价值,然而,一个显而易见的事实是,多个团队共享一个代码库可能会给业务带来沉重的成本。单一的、庞大的代码库也让工作变得毫无乐趣。
When many companies are now using continuous delivery and deploying value to their customers multiple times per day, it makes it painfully clear that a single codebase shared by multiple teams can result in heavy costs to the business. A single monolithic codebase is also the kind that is no fun to work on.
如果您有一个复杂的领域,实际上包含多个有界上下文,但您只有一个代码库,那么每个模型的边界不可避免地会不完整。一个有界上下文中的代码会与另一个有界上下文中的代码耦合,从而导致紧密的依赖关系。如前所述,一旦在有界上下文之间引入依赖关系,就会引入摩擦,阻碍它们独立发展,也阻碍每个团队的最佳工作。
If you have a complex domain that effectively has multiple bounded contexts, but you have only one codebase, it is inevitable that boundaries of each model will not remain intact. Code from one bounded context will become coupled to code in another, leading to tight dependencies. As previously mentioned, once you introduce dependencies between bounded contexts you introduce friction that stops them evolving independently and each team working optimally.
或者,如果您为每个有界上下文分配单独的项目或模块,则可以消除耦合的可能性。您可能在两个有界上下文中拥有看起来非常相似的代码,并且您可能会觉得违反了“不重复自己”(DRY)原则,但很多时候这并不是问题。您经常会发现,即使代码一开始看起来相同,但随着新概念和见解的出现,它会在每个有界上下文中由于不同的原因而发生变化。通过避免耦合有界概念,当您尝试整合新概念和见解时就不会产生摩擦。
Alternatively, if you have separate projects or modules for each bounded context, you remove the possibility of coupling. You might have code in two bounded contexts that look very similar, and you may feel you are violating the Don’t Repeat Yourself (DRY) principle, but a lot of the time that is not a problem. Very often you will find that even though the code looks the same to begin with, it changes in each bounded context for different reasons as new concepts and insights emerge. By not coupling the bounded concepts, there is no friction when you try to incorporate the new concepts and insights.
即使代码完全相同且从未更改,重复通常也不会造成问题。重复之所以会造成问题,是因为您可能在一个地方更新了代码,却忘记在另一个地方更新。然而,当您拥有松耦合、旨在独立运行的限界上下文时,这种情况很少会成为问题。很少有理由同时更改两个限界上下文中的相同概念。因此,不必担心重复类似的代码,而应专注于隔离限界上下文并维护其边界。
Even if the code is the same and it never changes, usually the duplication causes no problem. Duplication is a problem because you may update code in one place, and forget to update it in another. However, this is rarely a problem when you have loosely coupled, bounded contexts that are intended to run in isolation. There are very few reasons that the same concept in two bounded contexts should be changed at the same time. So don’t worry about duplicating similar code, and instead focus on isolating your bounded context and maintaining their boundaries.
为了保持有界上下文的完整性并确保其自主性,最广泛使用的方法是使用无共享架构,其中每个有界上下文都有自己的代码库、数据存储和开发团队。如果正确实施,这种方法可以构建一个由垂直组件组成的系统,如图11.3所示。
To retain the integrity of your bounded contexts and ensure they are autonomous, the most widely used approach is to use a shared-nothing architecture, where each bounded context has its own codebases, datastores, and team of developers. When done correctly, this approach results in a system composed of verticals, as demonstrated in Figure 11.3.
图 11.3具有无共享架构的自主有界上下文。
FIGURE 11.3 Autonomous bounded contexts with a shared-nothing architecture.
当每个有界上下文在物理上相互隔离时,一个有界上下文中的开发人员就无法再调用另一个有界上下文中的方法,也无法将数据存储在共享模式中。由于存在明显的物理隔离,他们不得不费力地引入耦合。更可能的是,他们会因为额外的工作而放弃,或者在创建不必要的依赖之前被其他团队成员提醒。
When each bounded context is physically isolated, no longer can developers in one bounded context call methods on another, or store data in a shared schema. Since there is a distinct physical separation, they have to go out of their way to introduce coupling. More than likely, they will be put off by the extra effort or brought up to speed by another team member before they can create an unnecessary dependency.
一旦有界上下文被清晰地隔离,并显著降低耦合的可能性,其他外部因素就不太可能影响模型。例如,如果在一个有界上下文中添加或改进了一个概念,它将不会影响其他有界上下文中类似的概念。如果只有一个代码库,这些概念很容易受到影响。从根本上说,清晰的物理隔离使得每个有界上下文仅因内部原因而演进,从而形成一个不受影响的领域模型,并在短期和长期内更高效地交付业务价值。
Once bounded contexts are clearly isolated and the potential for coupling is significantly reduced, other external factors are unlikely to influence the model. For example, if a concept is added or refined in one bounded context it will not affect concepts that look similar in other bounded contexts, which could easily get caught up in the changes had there been a single codebase. Fundamentally, the clear physical separation allows each bounded context to evolve only for internal reasons, resulting in an uncompromised domain model and more efficient delivery of business value in the short and long term.
当您面临集成由遗留代码组成的有界上下文的限制时,可以使用多种模式来限制遗留代码对系统其他部分的影响。这些模式可以帮助您管理复杂性,并避免为了集成遗留组件而降低新代码的显式性。
When you are faced with the constraints of integrating bounded contexts that are comprised of legacy code, there are a number of patterns you can use to limit the impact of the legacy on other parts of the system. These patterns help you manage the complexity and save you from having to reduce the explicitness of your new code in order to integrate the legacy components.
对于不熟悉 DDD 但想要将其应用于遗留系统的团队,建议考虑使用气泡上下文。由于气泡上下文与现有代码库隔离,它们为创建和演进领域模型提供了一个干净的平台。请记住,当您完全控制领域模型,并能够在获得新的领域洞察后自由地对其进行频繁迭代时,DDD 的效果最佳。即使涉及遗留代码,气泡上下文也能促进频繁迭代。
Teams that are unfamiliar with DDD but want to begin applying it to a legacy system are advised to consider using a bubble context. Because bubble contexts are isolated from existing codebases, they provide a clean slate for creating and evolving a domain model. Remember, DDD works best when you have full control over the domain model and are free to frequently iterate on it as you gain new domain insights. A bubble context facilitates frequent iteration even when legacy code is involved.
为了使气泡上下文有效,气泡和遗留模型之间需要一个转换层。DDD 中的反腐败层 (ACL) 概念非常适合这一需求,如图11.4所示。
For bubble contexts to be effective, a translation layer is necessary between the bubble and the legacy model(s). The DDD concept of an anti-corruption layer (ACL) is ideal for this need, as shown in Figure 11.4.
图 11.4气泡上下文。
FIGURE 11.4 A bubble context.
在构建气泡上下文时,ACL 的设计和实现是一项关键活动。它需要将遗留系统的细节与气泡完全隔离,同时准确地将气泡中的查询和命令转换为遗留模型中的查询和命令。然后,它必须将遗留系统的响应映射到气泡所需的格式。因此,ACL 本身可能是一个复杂的组件,需要大量的持续投入。
Design and implementation of the ACL is a key activity when building a bubble context. It needs to keep details of the legacy system completely isolated from the bubble while at the same time accurately translating queries and commands from the bubble into queries and commands in the legacy model. It then has to map the response from the legacy into the format demanded by the bubble. Accordingly, the ACL itself can be a complex component that requires a lot of continued investment.
如果你想与遗留代码集成,但又不想创建过于依赖遗留代码的气泡上下文,那么你可以使用自主气泡。而气泡上下文从遗留系统获取所有数据,自治气泡更加独立 - 拥有自己的数据存储,并且能够独立于遗留代码或其他有界上下文运行,如图11.5所示。
If you want to integrate with legacy code, but do not want to create a bubble context that is so dependent on the legacy code, you can instead use an autonomous bubble. Whereas a bubble context gets all its data from the legacy system, an autonomous bubble is more independent—having its own datastore(s) and being able to run in isolation of the legacy code or other bounded contexts, as shown in Figure 11.5.
图 11.5自主气泡环境。
FIGURE 11.5 An autonomous bubble context.
有时,自治气泡上下文的独立性的关键在于与其他新旧上下文的异步通信。因此,ACL(同步 ACL)通常承担执行异步通信的角色,如图11.5所示。
Sometimes crucial to the autonomous bubble context’s independence is asynchronous communication with other new and legacy contexts. Consequently, the ACL—a synchronizing ACL—often takes on the role of carrying out the asynchronous communication, as also illustrated in Figure 11.5.
由于自治气泡上下文拥有自己的数据存储,因此无需更新旧代码库或架构。任何新数据都可以存储在自治气泡的数据存储中。在选择气泡还是自治气泡时,这一点非常重要。然而,异步同步的成本和复杂性可能会显著增加。
Since the autonomous bubble context has its own datastore it does not require updating legacy codebases or schemas. Any new data can be stored in the autonomous bubble’s datastore. This is an important characteristic to keep in mind when deciding between the bubble and the autonomous bubble. However, the costs and complexity of asynchronous synchronization can be significantly higher.
当多个新上下文需要使用遗留系统时,为每个上下文创建专用 ACL 的成本可能会过高。您可以将遗留上下文公开为服务,这样新上下文就无需进行太多转换。一种常见且通常简单的方法是公开一个返回 JSON 的 HTTP API,如图11.6所示。这正式称为“开放主机”模式。
When a legacy system needs to be consumed by multiple new contexts the cost of creating a dedicated ACL for each context can be excessive. Instead, you can expose the legacy context as a service that requires less translation by the new contexts. A common, and often simple approach, is to expose an HTTP API that returns JSON, as shown in Figure 11.6. This is formally known as the Open Host pattern.
图 11.6将遗留上下文公开为 JSON Web 服务。
FIGURE 11.6 Exposing a legacy context as a JSON web service.
每个消费上下文仍然必须将来自旧上下文的响应转换为其自身的内部模型。然而,这种情况下转换的复杂性应该可以通过开放主机提供的 API 的简单性来减轻。
Each consuming context must still translate the response from the legacy context into its own internal model. However, the translation complexity in this scenario should be mitigated by the simplicity of the API provided by the open host.
虽然在有多个消费者的情况下,将遗留上下文公开为服务可能更高效,但两种气泡方法仍然值得考虑。将遗留上下文公开为服务有两个主要缺点:首先,需要对遗留上下文进行修改,而使用气泡方法则可能不需要修改。其次,公开一种易于多个消费者使用的格式可能具有挑战性。
While exposing legacy contexts as services can be more efficient when there are multiple consumers, both of the bubble approaches should still be considered. Two of the main drawbacks in exposing legacy contexts as services are that, first, modifications are required to the legacy context that may not be necessary with a bubble. Second, exposing a format that is easily consumable by multiple consumers may be challenging.
诚然,选择遗留系统集成策略可能会令人困惑。然而,通过三种方法的选择,您可以看到,在遗留系统上充分利用 DDD 的优势是完全可能的。
Admittedly, choosing a legacy integration strategy can be confusing. However, with three approaches to choose from, you can see that harnessing the benefits of DDD on legacy systems is definitely possible.
互联网用户数量的惊人增长意味着当今许多应用程序需要能够支持海量网络流量。如果无法支持如此多的用户,企业就无法最大限度地发挥其收入潜力。主要问题在于硬件:一台价格实惠的服务器通常不足以支持一个热门网站的所有用户。因此,负载需要分散到多台机器上。这不仅仅是网站,而是构成系统的所有后端服务。
The phenomenal growth in the number of people using the Internet means that many applications today need to be able to support huge levels of web traffic. If the number of users cannot be supported, a business will not maximize its revenue potential. Primarily, the problem is a hardware one: A single affordable server is usually not powerful enough to support all the users that a popular website may have. Instead, the load needs to be spread across multiple machines. And it’s not just websites, it’s all of the back end of services that make up a system.
将负载分散到多台机器上是一个常见问题,它催生了云计算的蓬勃发展。企业正在使用云托管解决方案来快速有效地扩展其系统成本。如果您将系统设计为可以分散到多台机器上,那么您可以利用云托管来高效地扩展系统。
Spreading load across multiple machines is such a common problem that it has given rise to the cloud boom. Businesses are using cloud-hosted solutions to rapidly scale their system’s price efficiently. If you design your systems so that they can be spread across multiple machines, you can take advantage of cloud hosting to efficiently scale your systems.
现代系统分布式的另一个关键原因是容错能力。如果一台服务器发生故障或出现问题,其他服务器必须能够承担增加的负载,以避免最终用户受到影响。
Another key reason that modern systems are distributed is fault tolerance. If one server fails or develops a problem, other servers must be able to take on the increased load to avoid end users suffering.
分布式系统意味着需要将其分解成更小的可部署组件。这对维护 DDD 所追求的明确性提出了挑战。
Having to distribute a system introduces the need to break it up into smaller deployable components. This poses a challenge to maintaining the explicitness that DDD strives for.
分布式系统虽然有助于解决高可扩展性问题,但也存在一系列自身问题。幸运的是,您可以选择要面对哪些问题,因为有许多不同的集成策略。远程过程调用 (RPC) 和异步消息传递是常见的选项,涵盖了本章的大部分内容。然而,有时共享文件或数据库也是一个不错的选择。
Distributed systems, although helping to solve the problems of high scalability, come with their own set of problems. Luckily, you do have some choice about which set of problems you need to face, because there are a number of different integration strategies. Remote procedure call (RPC) and asynchronous messaging are prevalent options and encompass most of this chapter. However, sometimes sharing files or databases can be a good enough alternative.
分布式系统带来了非功能性需求:可扩展性、可用性和可靠性。可扩展性是指能够支持不断增加的负载(例如更多并发用户)的能力。可用性与应用程序在线、运行和支持用户的频率有关。另一个考虑因素是可靠性,它与系统处理错误的能力有关。您很快就会发现,这些需求通常与系统的耦合度和复杂度相权衡。
Distributed systems bring nonfunctional requirements to the table: scalability, availability, reliability. Scalability is the ability to be able to support increasing loads, such as more concurrent users. Availability is concerned with how often the application is online, running, and supporting its users. Another consideration is reliability, which is concerned with how well a system copes with errors. You’ll see shortly that these requirements are often traded off with the amount of coupling in a system and the level of complexity.
在集成有界上下文时,务必了解业务方的非功能性需求,以便选择能够以最少的工作量满足这些需求的集成策略。某些选项(例如消息传递)的实现需要更多工作量,但它们为实现高可扩展性和可靠性奠定了坚实的基础。另一方面,如果您对可扩展性要求不高,则可以使用数据库集成,在初期投入较少精力即可集成有界上下文。之后,您就可以更快地交付其他重要功能。
When integrating your bounded contexts, it’s important to get an idea from the business what its nonfunctional requirements are so that you can choose an integration strategy that lets you meet them with the least amount of effort. Some options, such as messaging, take more effort to implement, but they provide a solid foundation for achieving high scalability and reliability. On the other hand, if you don’t have such strong scalability requirements, you can integrate bounded contexts with a small initial effort using database integration. You can then get on with shipping other important features sooner.
遗憾的是,你不能直接问业务人员系统需要多大的可扩展性或多高的可靠性。试着给他们提供各种场景,并解释每种场景的成本。例如,你可以告诉他们,你可以保证 99.9999% 的可靠性,但成本是保证 99.99% 的三倍。
Unfortunately, you can’t just ask the business how scalable a system needs to be or how reliable it should be. Try to give them scenarios and explain the costs of each scenario. For example, you could tell them that you can guarantee 99.9999% reliability, but it will cost triple the amount of guaranteeing 99.99%.
集成有界上下文的一种可行方法是允许一个应用程序写入另一个应用程序读取的特定数据库位置。您可能希望在首次迭代的最小可行产品 (MVP) 或系统中非性能关键部分采用这种方法。
An accessible approach to integrating bounded contexts is letting one application write to a specific database location that another application reads from. It’s likely that you would want to employ this approach in first-iteration Minimum Viable Products (MVPs) or nonperformance critical parts of the system.
例如,当您下订单时,销售边界上下文可以将其添加到SQL 数据库中的销售表中。稍后,计费边界上下文会检查表中是否有新记录。如果发现有新记录,它会为每条记录处理付款。
As an example, when you place an order, the Sales bounded context could add it to the Sales table in a SQL database. Later on, the Billing bounded context would come along and verify whether any new records had been added to the table. If it found any, it would then process the payment for each of them.
实现此解决方案有几种可能性。最可能的例子是计费限界上下文以特定频率(例如每 5 分钟)轮询表,并通过更新同一行中的列(可能是paymentProcessed)来跟踪已处理的订单。您可以在图 11.7中看到此示例的直观表示。
Implementing this solution has a few possibilities. The most likely example involves the Billing bounded context polling the table at a certain frequency, such as every 5 minutes, and keeping track of which orders it has processed by updating a column in the same row—paymentProcessed perhaps. You can see a visual representation of this in Figure 11.7.
图 11.7数据库集成。
FIGURE 11.7 Database integration.
数据库集成也具有一些松散耦合的优势。由于两个系统都通过写入和读取数据库进行通信,因此每个系统的实现都可以自由更改,只要它与现有模式保持兼容。但是,由于两个系统耦合到同一个数据库,因此随着系统的增长,这种解决方案可能会开始对您造成损害。数据库锁只是您可能会遇到的一个棘手问题。随着系统的一部分将越来越多的订单添加到表中,然后由另一部分进行更新,这两个系统将争夺数据库资源。数据库很可能成为单点故障 (SPOF),这意味着如果它停止运行,两个应用程序都会受到影响。某些数据库(例如 SQL Server)很难进行集群,因此您必须不断购买更大、更昂贵的硬件才能扩展。
Database integration also has some loose coupling benefits. Because both systems communicate through writing and reading to a database, the implementation of each system is free to change providing it maintains compatibility with the existing schema. However, the systems are coupled to the same database, so this style of solution can really start to hurt you as the system grows. Database locks are just one painful problem you might come up against. As increasingly more orders are added to the table by one part of the system and then updated by another, the two systems will be competing for database resources. The database will likely be a single point of failure (SPOF), meaning that if it grinds to a halt, both applications will suffer. Some databases, like SQL Server, are hard to cluster, so you have to keep buying bigger, more expensive hardware to scale.
另一个缺点是,数据库集成无法引导你找到处理故障的良好解决方案。如果销售限界上下文在保存订单之前崩溃了怎么办?订单会丢失吗?如果数据库宕机了怎么办?这是否意味着公司无法接受订单?这些都是使用数据库集成时你必须自己设计解决方案的大问题。
Another drawback is that database integration doesn’t guide you into a good solution for handling faults. What if the Sales bounded context crashes before saving an order? Is the order lost? What if the database goes down? Does that mean the company cannot take orders? These are big problems that you are left on your own to devise solutions for when using database integration.
如果您的项目中没有使用数据库,那么仅仅为了集成两个组件而设置数据库可能会带来不必要的开销。这是一个平面文件集成可能就足够好的例子。一个组件将文件放在某个服务器上,而另一个应用程序稍后再获取它们,类似于数据库集成的方式。平面文件集成是一种比数据库集成更灵活的方法,但您必须更具创造力,这反过来意味着在重要的业务功能上需要付出更多努力并延长交付周期。在图 11.8中,您可以看到一种平面文件集成替代方案的可能实现,该方案可以替代图 11.8中的数据库集成解决方案。
If you aren’t using a database in your project, setting one up just to integrate two components can be unnecessary overhead. This is one example in which flat file integration may be good enough. One component puts files on a server somewhere, while another application picks them up later, in a similar way to database integration. Flat file integration is a more flexible approach than database integration, but you have to be more creative, which can in turn mean more effort and slower lead times on important business functionality. In Figure 11.8, you can see one possible implementation of a flat file integration alternative to the database integration solution in Figure 11.8.
图 11.8平面文件集成。
FIGURE 11.8 Flat file integration.
平面文件集成保留了数据库集成的松耦合特性,避免了数据库锁定问题。遗憾的是,您必须付出更多努力才能弥补这一点。其中一个弥补方面涉及文件格式。由于没有模式或标准查询语言,您需要自行创建文件格式,并确保所有应用程序都能理解并正确使用。由于这需要更多手动操作,因此确实会增加出错的可能性,但这很大程度上取决于您的具体情况。
Flat file integration retains the loose coupling features of database integration without suffering from the database locking problem. Unfortunately, you have to work harder to compensate for this. One area of compensation involves the file format. Because there is no schema or standard query language, you are responsible for creating your own file format and ensuring that all applications understand it and use it correctly. Because this is more manual work, it does increase the possibility for error, although a lot depends on your circumstances.
由于平面文件集成是一种自行解决的解决方案,因此没有可扩展性或可靠性方面的指导原则。它完全取决于您选择的技术以及您如何实现它们。如果您确实需要一种可扩展的方法,那么投入所有这些精力可能会造成巨大的浪费,并且巨大的风险。接下来讨论的 RPC 是一种常见的替代方案,它在可扩展性和可靠性特性方面相对知名。
Because flat file integration is a do-it-yourself solution, there are no scalability or reliability guidelines. It’s completely dependent on the choice of technologies you use and how you implement them. If you do need an approach that scales, going to all that effort can be a massive waste and a massive risk. RPC, discussed next, is a common alternative that is a relatively well known quantity in terms of scalability and reliability characteristics.
想象一下,如果您可以保持单体应用几乎完全相同,同时又能获得分布式系统的可扩展性优势,那会是怎样一番景象。这正是使用 RPC 的动力所在。使用 RPC 时,任何方法调用都可能通过网络调用其他服务;除非您查看具体实现,否则无法判断。您能知道运行以下代码片段会产生多少次网络调用吗?
Imagine if you could keep your monolithic almost identical but get the scalability benefits of a distributed system. This is the motivating force behind the use of RPC. When using RPC, any method invocation could call another service across the network; it’s not possible to tell unless you look at the implementation. Can you tell how many network calls would occur if you ran the following code snippet?
var order = salesBoundedContext.CreateOrder(orderRequest);
var paymentStatus = billingBoundedContext.ProcessPaymentFor(order);
if (paymentStatus.IsSuccessful)
{
shippingBoundedContext.ArrangeShippingFor(order);
}
var order = salesBoundedContext.CreateOrder(orderRequest);
var paymentStatus = billingBoundedContext.ProcessPaymentFor(order);
if (paymentStatus.IsSuccessful)
{
shippingBoundedContext.ArrangeShippingFor(order);
}
由于您不知道被调用的任何方法内部发生了什么,因此无法判断前面的代码片段中发生了多少次网络调用。这些调用可能是内存逻辑,也可能是对执行该逻辑的其他应用程序的网络调用。RPC 的支持者认为这一特性具有巨大的优势,因为它是一种侵入性最小的解决方案。在某些情况下,RPC 可能是最佳选择。
It’s impossible to tell how many network calls occur in the previous code snippet because you don’t know what happens inside any of the methods that are called. There could be in-memory logic, or there could be network calls to other applications that carry out the logic. Proponents of RPC see this trait as a huge benefit because it is a minimally invasive solution. In some situations, RPC can be the best choice.
选择 RPC 时,您拥有很大的自由度,因为 RPC 本身是一个可以通过多种方式实现的概念。如果您与大量使用 RPC 的公司交流,您会发现他们使用大多数类型的 Web 服务,例如简单对象访问协议 (SOAP)、表述性状态转移 (REST)、可扩展标记语言 (XML)——使用各种不同的技术,例如 Windows Communication Foundation (WCF)。RPC 往往比平面文件集成更容易,因为大多数编程社区都有框架可以帮你处理大量的基础设施。
When choosing RPC, you have a lot of freedom, because RPC itself is a concept that you can implement in a variety of ways. If you were to talk to enough companies using RPC, you would find examples using most kinds of web service—Simple Object Access Protocol (SOAP), REpresentational State Transfer (REST), eXtensible Markup Language (XML)—using a variety of different technologies, such as Windows Communication Foundation (WCF). RPC tends to be easier than flat file integration because most programming communities have frameworks that deal with a lot of the infrastructure for you.
许多分布式系统新手倾向于使用 RPC,因为他们现有的大部分代码都可以复用。这无疑是选择 RPC 的一个有力理由,但同时也存在一些缺点。在后面的部分(“分布式系统中 DDD 的挑战”)中,您将详细了解为什么 RPC 的吸引力在实现可扩展性和可靠性方面存在一些深层次的缺陷。正因如此,异步、响应式消息传递解决方案才显得尤为重要。
Many distributed systems novices are tempted to use RPC because most of their existing code can be reused. This is a compelling case for choosing RPC, but it is also a drawback. In a later section (“The Challenges of DDD with Distributed Systems”), you learn in some detail why RPC’s appeal has some deep flaws for achieving scalability and reliability. That is why there is a need for asynchronous, reactive messaging solutions.
简而言之,网络是不可靠的。即使是像 Netflix 和 Amazon 这样的巨头公司,也会遭遇网络问题,导致系统宕机(http://www.thewhir.com/web-hosting-news/netflix-outage-caused-by-ec2-downtime-reports)。响应式解决方案试图通过使用异步消息传递模式进行通信来提高可靠性,从而拥抱故障。这意味着,当消息失败时,系统可以检测到并稍后重试(例如将其存储在队列中)或采取其他措施。
Quite simply, networks are unreliable. Even the biggest companies like Netflix and Amazon suffer from network problems that result in system outages (http://www.thewhir.com/web-hosting-news/netflix-outage-caused-by-ec2-downtime-reports). Reactive solutions try to embrace failure by increasing reliability using asynchronous messaging patterns for communication. This means that when a message fails, there is a way for the system to detect this and try it again later (such as storing it in a queue) or take a different course of action.
不幸的是,使用消息传递通常意味着,与 RPC 不同,你的代码看起来截然不同。当你查看 RPC 代码时,看不到任何网络方面的痕迹;而当你查看许多消息传递解决方案时,很明显代码是异步的,并且可能涉及网络。不仅如此,消息传递系统的整体设计和架构也截然不同,团队面临着令人望而生畏的学习曲线的挑战。幸运的是,你将在本章和下一章中学习消息传递,因为除了 RPC 之外,消息传递是构建分布式 DDD 系统的另一个常用选项。特别是,你将了解到消息传递的异步特性也为提高可扩展性提供了平台。
Unfortunately, using messaging usually means that, unlike RPC, your code looks drastically different. When you look at RPC code, there’s no hint of the network; when you look at many messaging solutions, it’s clear that code is asynchronous, and there’s probably a network involved. Not only that, but the entire design and architecture of messaging systems are significantly different, and teams are challenged with an intimidating learning curve. Fortunately, you will learn about messaging in this chapter and the next because, along with RPC, it is the other common option used for building distributed DDD systems. In particular, you will learn that the asynchronous nature of messaging also provides the platform for improved scalability.
如果您想要消息传递解决方案的可扩展性和可靠性优势,但又想使用超文本传输协议 (HTTP) 而不是消息传递框架,不妨尝试 REST。REST 会将您的端点建模为富超媒体资源,并充分利用 HTTP 的诸多优势,例如其动词和标头。然后,您可以使用 REST 在 HTTP 之上构建事件驱动系统,从而获得消息传递系统的诸多优势,有时还能减少问题。
If you want the scalability and reliability benefits of messaging solutions, but you want to use Hypertext Transport Protocol (HTTP) instead of messaging frameworks, try REST. REST involves modeling your endpoints as hypermedia-rich resources and using many of the benefits of HTTP, such as its verbs and headers. You can then build event-driven systems on top of HTTP with REST to get many of the benefits of a messaging system, and sometimes fewer of the problems.
REST 也是一个实用的工具,可以将您的系统公开为应用程序编程接口 (API),供其他应用程序集成。在本章学习分布式系统概念之后,您将在第 13 章“通过 HTTP 使用 RPC 和 REST 进行集成”中了解有关 REST 集成的更多信息。
REST is also a useful tool for exposing your system as an application programming interface (API) for other applications to integrate with. After learning about distributed systems concepts in this chapter, you will learn more about integrating with REST in Chapter 13, “Integrating via HTTP with RPC and REST.”
当你的有界上下文是独立的服务,它们通过网络相互通信时,你就拥有了一个分布式系统。在这些系统中,选择错误的集成策略可能会导致系统运行缓慢或不可靠,从而对业务产生负面影响。开发团队需要了解构建分布式系统的方法,以减少这些问题的可能性和严重性,并允许业务随着需求的增长而扩展。
When your bounded contexts are separate services that communicate with each other over the network, you have a distributed system. In these systems, choosing the wrong integration strategy might cause slow or unreliable systems that lead to negative business impacts. Development teams need to understand approaches to building distributed systems that can reduce the potential and severity of these problems and allow a business to scale as demand grows.
接受故障的发生并做好准备是构建分布式系统的一个关键方面,但这并不是 RPC 所固有的。
Accepting that failures happen, and preparing for them, is a critical aspect of building distributed systems, but one that is not inherent to RPC.
当需要将应用程序从单一代码库扩展到多个较小的子系统时,您可能会想用 HTTP 调用替换类的实现。这样,旧逻辑就会转移到新的子系统,也就是此 HTTP 调用的目标。这样做很诱人,因为您可以将负载分散到两台机器上,而无需更改太多代码。实际上,代码看起来是一样的,如前面的代码片段(如下所示)所示:
When the time comes to scale your application from a single codebase to a number of smaller subsystems, you might be tempted to replace the implementation of a class with an HTTP call. The old logic then moves to a new subsystem, which is the target of this HTTP call. It’s tempting because you can spread the load across two machines without having to change much code. In fact, the code looks the same, as the previous code snippet (repeated below) demonstrates:
var order = salesBoundedContext.CreateOrder(orderRequest);
var paymentStatus = billingBoundedContext.ProcessPaymentFor(order);
if (paymentStatus.IsSuccessful)
{
shippingBoundedContext.ArrangeShippingFor(order);
}
var order = salesBoundedContext.CreateOrder(orderRequest);
var paymentStatus = billingBoundedContext.ProcessPaymentFor(order);
if (paymentStatus.IsSuccessful)
{
shippingBoundedContext.ArrangeShippingFor(order);
}
每个方法调用可以完全在内存中处理,也可以通过 HTTP 调用另一个执行相应逻辑的服务。请记住,这就是 RPC 的目标:使网络通信透明化。如图 11.9所示,是一个使用 RPC 下单的电商系统的示例。
Each method call can be processed entirely in memory or can make an HTTP call to another service that carries out the logic. Remember that this is the goal of RPC: to make network communication transparent. Have a look at Figure 11.9 to see an example of an e-commerce system using RPC to place an order.
图 11.9使用同步 RPC 的电子商务系统。
FIGURE 11.9 E-commerce system using synchronous RPC.
虽然 RPC 感觉上很好地运用了面向对象编程和封装,但它也存在一些分布式系统社区多年来熟知的重大缺陷。这些缺陷很容易抵消您与领域专家频繁协作、精心构建领域模型所付出的辛勤努力。为了避免您遭受 RPC 带来的困扰,我们将先了解其固有问题,然后再向您展示尝试解决其缺点的替代方法。之后,您将能够自行决定 RPC 或消息传递是否是您项目的最佳选择。
Although RPC feels like good use of object-oriented programming and encapsulation, it has some significant flaws that the distributed systems community has known for years. These flaws can easily negate the determined effort you spent frequently collaborating with domain experts and finely crafting your domain models. To save you suffering from the pain caused by RPC, you will now learn its inherent problems before being shown alternative approaches that attempt to address its drawbacks. You will then be able to decide for yourself if RPC or messaging is the best choice for the projects you work on.
由于 RPC 使网络通信透明化,它会让你忘记网络的存在。然而,网络错误确实会发生,这意味着使用 RPC 的系统更有可能不可靠。网络错误一直是分布式系统中的主要问题根源,在《分布式计算的谬误》(http://blog.newrelic.com/2011/01/06/the-fallacies-of-distributed-computing-reborn-the-cloud-era/)一文中,网络错误被重点提及。本质上,事实一次又一次地证明,网络既不可靠,也存在带宽和延迟成本,而 RPC 实现往往将这些成本视为理所当然。
Because RPC makes network communication transparent, it encourages you to forget the network is there. Unfortunately, network errors do happen, meaning that systems using RPC are more likely to be unreliable. Network errors have been such a major source of problems in distributed systems that they feature prominently in the Fallacies of Distributed Computing (http://blog.newrelic.com/2011/01/06/the-fallacies-of-distributed-computing-reborn-the-cloud-era/). Essentially, time and again, it has been proven that networks are neither reliable nor free from bandwidth and latency costs that RPC implementations often take for granted.
在一个在线订单处理场景中,账单限界上下文会向第三方支付提供商发出 HTTP 调用。如果网络中断或支付提供商离线,订单将无法完成。此时,潜在客户会因为无法购买产品而感到不悦,而企业则会因为错失收入而更加不悦。想象一下繁忙的圣诞节或重大体育赛事;许多公司的商业模式很大一部分就是在这些关键事件中最大限度地利用机会。如果系统宕机,可能会对业务造成严重后果。您将在本章后面看到如何避免这些问题,即使发生重大故障也是如此。
In an online order processing scenario in which the Billing bounded context makes an HTTP call to a third-party payment provider, if the network is down or the payment provider goes offline, the order cannot be completed. At this point, the potential customer will be unhappy that she can’t purchase products, and the business will be even less happy considering it missed out on revenue. Consider the case of a busy Christmas period or a major sporting occasion; a large part of the business model for many companies is maximizing opportunity at these key events. If the system is down, there could be severe consequences for the business. You will see later in this chapter how to avoid these problems, even when major failures happen.
使用相同的电子商务场景,可以证明使用 RPC 的系统的可扩展性局限性。假设一位业务利益相关者向您发送一封电子邮件,表达了对用户投诉数量的担忧。您被告知数百名用户报告网站速度非常慢,而其他用户则报告说,有时由于超时错误,他们根本无法访问主页。这个问题是扩展性问题,因为当前系统无法支持当前的并发用户数量。
Using the same e-commerce scenario, the scalability limitations of systems that use RPC can be demonstrated. Consider the case in which a business stakeholder sends you an e-mail expressing concern at the number of complaints received from users. You are being told that hundreds of users are reporting a very slow website, while others are reporting that sometimes they completely fail to reach the home page at all because of timeout errors. The problem is a scaling issue because the current system cannot support the current number of concurrent users.
为了提高用户满意度并减少投诉数量,网站需要更快,并能够支持更多并发用户。不幸的是,销售、结算和运输部门如图 11.9所示,所有限界上下文都必须在用户收到响应之前进行一些处理。这意味着,为了让企业网站速度更快,所有限界上下文都需要增加资源。如果只是网站服务器速度更快,用户仍然需要等待相同的时间才能让每个限界上下文完成工作。这是不可取的,因为单纯地提高网站速度并非易事。
To improve user satisfaction and reduce the number of complaints, the website would need to be faster and able to support more concurrent users. Unfortunately, the Sales, Billing, and Shipping bounded contexts all have to do some processing before the user gets a response, as illustrated in Figure 11.9. That means all of them need increased resources in order to give the business a faster website. If just the website gets faster servers, the user will still be waiting the same amount of time for each of the bounded contexts to do their work. This is undesirable because it is not easy to just make the website faster.
与之前一样,也存在一些替代技术,允许企业按需高效地扩展网站或任何其他有界上下文。您将在本章后面了解这些技术。
As before, alternative techniques exist that allow the business to scale just the website, or indeed any other bounded context, in isolation on an efficient, as-needed basis. You will read about them later in this chapter.
使用 RPC 的系统具有紧密耦合的软件组件,这可能导致团队之间的耦合以及您刚刚读到的技术问题。当一个系统向另一个系统发出 RPC 调用时,实际上存在多种形式的紧密耦合,您确实需要注意。首先是逻辑耦合,因为发出调用的服务中的逻辑与接收调用的服务是耦合的。其次是时间耦合,因为发出调用的服务期望立即得到响应。
Systems that use RPC have tightly coupled software components that can lead to couplings between teams as well as technical problems like those you just read about. When a system makes an RPC call to another system, there are actually multiple forms of tight coupling that you really need to be aware of. First, there is a logical coupling, because the logic in the service making the call is coupled to the service receiving the call. Second, there is a temporal coupling, because the service making the call expects a response straight away.
移除对共享代码的依赖仍然会导致耦合。如果一个服务调用另一个服务,被调用的服务必须存在,并且必须以调用服务能够理解的方式运行。因此,调用服务与接收服务在逻辑上耦合。逻辑耦合带来的问题可能类似于共享代码依赖——一个地方的更改会破坏另一个地方的功能。当被调用的服务因故障而停止时,逻辑耦合也会带来问题,因为在这种情况下,两个服务都无法正常运行。这会损害可靠性。
Removing dependencies on shared code can still result in coupling. If a service makes a call to another service, the called service has to exist and has to behave in a way that the calling service understands. Therefore, the calling service is logically coupled to the receiving service. The kinds of pain caused by a logical coupling can be similar to a shared-code dependency—changes in one place break functionality in another. Logical couplings can also cause pain when the service being called goes down due to failure, because in those cases neither service is functioning. This hurts reliability.
性能是一项特性。研究表明,网站性能越高,用户转化为付费客户的可能性就越大。如果系统的某个部分需要提升速度以改善用户体验,那么如果该组件依赖另一个组件完成部分处理并立即响应,就会非常困难。这被称为时间耦合,是 RPC 的固有特性。图 11.9突出显示了时间耦合,显示用户在等待响应的同时,每个有界上下文轮流执行某些处理。这导致组件无法独立扩展。
Performance is a feature. The more performant a website is, the more research indicates that users will be converted into paying customers. If part of your system needs a speed increase to improve some aspect of user experience, it can be difficult if that component relies on another component to do some part of the processing and respond immediately. This is known as temporal coupling, and it is inherent to RPC. Figure 11.9 highlights temporal coupling, showing the user waiting for a response while each bounded context takes its turn to perform some processing. This leads to a component not being able to scale independently.
事务是维护数据一致性的最佳实践。然而,在构建分布式系统时,由于涉及网络通信,事务的成本很高。因此,诸如过多的长期数据库锁定或部分故障等原因可能会对可扩展性和可靠性产生负面影响。因此,在构建分布式系统时,您应该仔细考虑分布式事务以及 RPC。
Transactions are a best practice for maintaining data consistency. Unfortunately, when building distributed systems, transactions carry a high cost because of the involvement of network communication. As a result, scalability and reliability can be negatively impacted for reasons such as excessive long-held database locks or partial failures. Therefore, you should carefully consider distributed transactions, as well as RPC, when building distributed systems.
分布式事务的一个典型示例是预订包含酒店和航班的假期,您的系统存储酒店信息,但航班信息由外部系统处理。当您下订单时,数据库中的酒店房间信息会被锁定。该锁定会保留一段时间,直到航班预订完成。由于通过互联网向航空公司发出 HTTP 调用平均需要几秒钟,因此数据库锁定和连接的数量会不断增长。一旦数据库达到临界点,它们就会崩溃,拒绝新的连接,甚至出现更糟糕的情况。随着系统规模的扩大,这个问题的严重性只会增加,这意味着收入损失的可能性也会增加。
A typical example of a distributed transaction is booking a holiday that consists of a hotel and a flight, where your system stores hotels but flights are handled by an external system. When you place an order, a lock is put on the hotel room in your database. The lock is then held for a while until the flight is booked. Because it takes on average a few seconds to make the HTTP call over the Internet to the flight company, the number of database locks and connections grows. Once you reach a tipping point with databases, they tend to fall over and reject new connections or worse. As your system starts to scale up, the severity of this issue only increases, meaning that opportunities for revenue loss increase.
使用分布式事务时的另一个担忧是部分可用性。如果您锁定了酒店数据库记录,而航班提供商却离线了,该怎么办?在分布式事务中,您就会遇到故障,这意味着事务会中止或回滚。如果没有同时预订酒店和航班的特定业务需求,那么由于可避免的技术错误,这又会造成收入损失。
Another concern when using distributed transactions is partial availability. What if you lock a hotel database record and find the flight provider is offline? In a distributed transaction, you have a failure, meaning the transaction aborts or rolls back. If there’s no specific business requirement for hotels and flights to be booked at the same time, this is again a loss of revenue due to avoidable technical errors.
那么,除了使用数据库锁之外,您还能做什么来防止不良情况发生,例如在没有航班的情况下预订酒店房间?一种常见的方法是直接前滚到可以解决问题的新状态。在假期预订场景中,这意味着当航班预订失败时取消酒店预订。但这不会在单个事务中发生。这意味着您的有界上下文将具有不一致的世界观。订单可能存在于其中一个上下文中,但不存在于另一个上下文中。在系统的其他部分,用户可能已经更新了他的地址,但更新尚未到达其他有界上下文。这通常不是理想的选择,但您必须记住,您越想扩展,就越需要做出此类权衡。
So what do you do instead of using database locks to prevent undesirable scenarios occurring, like booking a hotel room when there is no flight available? A common approach is to just roll forward into a new state that corrects the problem. In the holiday booking scenario, that would mean cancelling the hotel reservation when the flight booking fails. But this would not happen inside a single transaction. What this does mean is that your bounded contexts will have inconsistent views of the world. An order might exist in one of them, but not in another. In other parts of the system, a user might have updated his address, but the update hasn’t reached other bounded contexts yet. This often isn’t ideal, but you have to remember that the more you want to scale, the more you may have to make these kinds of trade-offs.
通过避免分布式事务,您可以处理部分故障而不会造成收入损失。例如,一旦预订了酒店,即使航班提供商离线或出现故障也无关紧要。您可以在提供商恢复在线或正常运行时预订航班。您将在本章和下一章的后面部分看到此类示例。
By avoiding distributed transactions, you can handle partial failures without incurring revenue loss. Once the hotel is booked, for example, it doesn’t matter if the flight provider is offline or in some failure mode. You can book the flight when the provider comes back online or is working properly. You’ll see examples of this later in this chapter and the next.
允许系统存在暂时的不一致并非激进的做法。这在分布式系统中是一个相当常见的概念,被称为最终一致性。
Allowing temporary inconsistencies in your system is not a radical approach. It’s quite a common concept in distributed systems and goes by the name of eventual consistency.
尽管你的系统可能处于不一致的状态,但目标始终是使每条数据在某个时刻达到一致性。(整个系统可能永远不会处于完全一致的状态)。在在前面的例子中,感知一致性是通过在提供商恢复上线时安排航班来实现的。本质上,这就是最终一致性,但它适用于更广泛的场景。
Although your system may be in inconsistent states, the aim is always to reach consistency at some point for each piece of data. (The system overall will probably never be in a fully consistent state). In the previous example, perceived consistency was achieved by arranging the flight when the provider came back online. Essentially, this is eventual consistency, but it applies to a much wider range of scenarios.
与最终一致性相关的一个重要缩写词是 BASE,它代表“基本可用、软状态、最终一致性”。这与您可能熟悉的关系型数据库的 ACID(原子性、一致性、隔离性、持久性)形成了对比。这些缩写词突出了两种方法在一致性语义上的根本区别。
One important acronym that goes along with eventual consistency is BASE, which stands for Basically Available, Soft state, Eventual consistency. This is in contrast to ACID (Atomicity, Consistency, Isolation, Durability), which you are probably familiar with from relational databases. These acronyms highlight the fundamental differences in consistency semantics between the two approaches.
在下一章中,您将看到一些示例,了解如何构建满足最终一致性要求的消息传递系统,同时尽量不损害用户体验。然而,牺牲用户体验可能是使用最终一致性的一大弊端。网站允许您下订单或添加其他信息,但无法立即获得确认的情况很常见。这会让一些用户感到不满,而且在很多情况下,这种感觉理所当然。但也有很多成功案例,从像亚马逊这样的大公司(http://cacm.acm.org/magazines/2009/1/15666-eventually-consistent/fulltext)到规模小得多的公司。如果您做好充分的准备并认真规划,那么您就很有可能获得最终一致性带来的可扩展性优势,同时仍然提供出色的用户体验。
In the next chapter you see examples of how to build messaging systems that cater to eventual consistency in a way that tries not to hurt user experience. However, sacrificing user experience can be one of the big drawbacks to using eventual consistency. It’s common for websites to allow you to place an order or add some other piece of information yet not have immediate confirmation. This upsets some users, and rightly so in a lot of cases. But there are also many success stories, from large companies like Amazon (http://cacm.acm.org/magazines/2009/1/15666-eventually-consistent/fulltext) to much smaller companies. If you do your homework and plan diligently, you give yourself a great chance of getting the scalability benefits of eventual consistency while still providing a great user experience.
除了下一章中的示例之外,我们鼓励您在构建实际使用最终一致性的系统之前,花一些时间深入了解它。推荐您阅读 Pat Helland 的颇具影响力的论文《超越分布式事务的生活:一个叛教者的观点》(http://cs.brown.edu/courses/cs227/archives/2012/papers/weaker/cidr07p15.pdf)。Martin Fowler 也写了一篇关于无惧无事务生活的好文章(http://martinfowler.com/bliki/Transactionless.html)。
Aside from the example in the next chapter, you are encouraged to spend a bit of time learning more about eventual consistency before you build a real system that uses it. Pat Helland’s influential paper “Life Beyond Distributed Transactions: An Apostate’s Opinion” is recommended reading (http://cs.brown.edu/courses/cs227/archives/2012/papers/weaker/cidr07p15.pdf). Martin Fowler also has a good piece on not being afraid of living without transactions (http://martinfowler.com/bliki/Transactionless.html).
20 多年来,分布式系统专家们早已意识到同步 RPC 的局限性,因此在很多情况下更倾向于采用异步的事件驱动型消息传递解决方案 ( http://armstrongonsoftware.blogspot.co.uk/2008/05/road-we-didnt-go-down.html )。在深入探讨这种方法为何更具优势之前,我们将先了解如何使用它来解决上一个 RPC 示例中的弹性和可扩展性问题。
For more than 20 years, distributed systems experts have known about the limitations of synchronous RPC and instead preferred asynchronous, event-driven messaging solutions in many situations (http://armstrongonsoftware.blogspot.co.uk/2008/05/road-we-didnt-go-down.html). Before delving into the fundamentals of why this approach can be more beneficial, you will see how to use it to remedy the resilience and scalability issues of the previous RPC example.
在本节中,您将学习如何选择良好的集成策略,从而带来巨大的可扩展性和可靠性优势,并最终对业务产生积极的影响。您将看到一种基于响应式编程原则的替代解决方案。(http://www.reactivemanifesto.org/)——一种用异步消息取代 RPC 的理念。图 11.10展示了图 11.9中 RPC 实现的替代反应式消息传递的设计。
In this section, you learn how choosing a good integration strategy can have massive scalability and reliability benefits that make a positive business impact. What you are about to see is an alternative solution using the principles of reactive programming (http://www.reactivemanifesto.org/)—a philosophy that replaces RPC with asynchronous messages. Figure 11.10 shows the design of a reactive messaging alternative to the RPC implementation shown in Figure 11.9.
图 11.10用反应式替换 RPC。
FIGURE 11.10 Replacing RPC with reactive.
在图 11.10中,您可以看到,当用户在网站上下订单时,会向账单边界上下文异步发送一条消息,指示订单已下达。随后,账单边界上下文会在下达后发出一个异步的PaymentAccepted事件。与支付提供商进行通信。Shipping 限界上下文订阅PaymentAccepted事件,在收到订单后安排发货。同时,与网站交互的用户在下单意图被捕获后会立即得到响应。所有信息确认后,他会收到一封电子邮件。异步消息无需等待流经限界上下文。性能问题通过异步处理订单得到了解决,但弹性问题又该如何解决呢?
In Figure 11.10, you can see that when a user places an order with the website, a message is asynchronously sent to the Billing bounded context indicating that an order has been placed. In turn, the Billing bounded context emits an asynchronous PaymentAccepted event after communicating with the payment provider. The Shipping bounded context subscribes to the PaymentAccepted event, arranging shipping for an order when one is received. Meanwhile, the user interacting with the website gets an immediate response as soon as his intent to place an order is captured. He then gets an e-mail later once everything is confirmed. There’s no wait for the asynchronous messages to flow through the bounded contexts. The performance problem is solved by processing the order asynchronously, but what about the resiliency problems?
为了解决弹性问题,每条消息都会被放入队列中,直到接收者成功处理为止。现在,重新考虑一下在处理某人下单意图时可能出现的问题。如果支付提供商遇到问题,消息会存储在队列中,并在支付提供商可用时重试。如果其中一个有界上下文出现硬件故障或包含编程错误,消息会再次进入队列,等待有界上下文恢复正常。正如您所见,响应式解决方案提供了卓越的弹性平台,尤其是与 RPC 替代方案相比。
To solve the resiliency problems, each message is put in a queue until the recipient(s) has successfully processed it. Now reconsider the problems that can occur during the processing of someone’s intent to place an order. If the payment provider is experiencing problems, the message is stored in a queue and retried when the payment provider is available. If one of the bounded contexts has a hardware failure or contains a programming bug, the message again sits in a queue waiting for the bounded context to come back online. As you can now see, reactive solutions provide the platform for exceptional levels of resilience, particularly compared to RPC alternatives.
为了进一步理解响应式解决方案如何提升可扩展性,请回想一下用户报告网站速度极慢的案例。您可以通过向负载均衡器添加更多 Web 应用程序实例,轻松实现最佳的横向扩展解决方案,而无需更改用于有界上下文的硬件。当系统出现瓶颈时,您可以更精细地选择在哪里添加新硬件。最终,这意味着企业可以更经济高效地利用硬件或云资源。在之前的例子中,由于编程方式的限制,甚至可能无法横向扩展每个有界上下文。这意味着必须纵向扩展它们,而这相对来说成本更高。
To further understand how the reactive solution increases scalability, think back to the case in which users are reporting that the website is unusably slow. You can easily achieve the optimal solution of scaling out by adding more instances of the web application to a load balancer without changes to the hardware used for the bounded contexts. Whenever bottlenecks occur in the system, you have more granular choices to make about where to add new hardware. Ultimately, this means the business makes more cost-effective use of hardware or cloud resources. In the old example, it might not even be possible to scale out each of the bounded contexts because of the way they have been programmed. This means they would have to be scaled up, which can be relatively more expensive.
切勿轻易形成“响应式编程能解决所有问题、让生活变得轻松”的印象,因为您仍然需要付出大量努力。与软件开发中需要做出的许多决策一样,采用响应式方法既有积极的影响,也有消极的影响,您需要根据当前面临的限制进行权衡。以下是构建响应式应用程序时可能面临的一些挑战:异步解决方案的调试难度增加、代码在其他人尝试理解其工作原理时显得更加间接,以及最终一致性问题。此外,还需要面对更多用于传递和重试消息的基础设施组件,这会增加复杂性。
Be careful about forming the impression that reactive programming solves all your problems and makes life easy, because a lot of effort on your part is still required. As with many decisions you need to make in software development, taking the reactive approach has positive and negative consequences that you need to trade off against the constraints you are currently working to. Here are some of the challenges you are likely to face when building reactive applications: increased difficulty in debugging your asynchronous solution, more indirection in your code when others try to understand how it works, and eventual consistency. You also have the added complexity of more infrastructure components that deliver and retry messages.
不过,不必太担心学习新技术。相反,要保持兴奋。下一章将提供具体示例,展示基础设施技术如何帮助您调试异步系统。您甚至会发现,通过合理的命名和代码结构约定,弄清楚异步代码的功能并非难事。此外,我们还将概述一个概念框架,帮助您顺利掌握最终一致性。
Don’t worry too much about having to learn new techniques, though. Instead, be excited. You will see concrete examples in the next chapter of how infrastructure technologies assist you in debugging asynchronous systems. You will even see that, by having sensible naming and code structure conventions, working out what asynchronous code is doing doesn’t have to be such a huge problem. A conceptual framework will also be outlined that puts you well on your way to coming to grips with eventual consistency.
您应该将 RPC 视为特定情况下的最佳选择。它会带来可扩展性和可靠性问题,但有时这些问题可能并非最重要的制约因素。第 13 章将向您展示如何为使用 RPC 的 DDD 系统构建基于 HTTP 的服务,以便您能够在下文描述的场景中应用它们。
You should consider RPC as a tool that might be the best choice for certain situations. It poses scalability and reliability problems, but sometimes they might not be the most important constraints. Chapter 13 shows you how to build HTTP-based services for DDD systems that use RPC so that you can apply them in the situations described next.
有时,您只需要将新功能或新产品发布到互联网上供客户评估。企业可能希望击败竞争对手,也可能只是想评估用户对产品的反馈。在这些情况下,可扩展性和可靠性并非问题。因此,如果您认为构建基于 RPC 的解决方案能够满足这些要求,那么您应该寻找一个不使用它的充分理由。它可能为您带来上市优势的一个原因是,与消息传递框架和概念相比,更多的开发人员熟悉 HTTP。因此,您可以更快地招聘人员,他们可以更快地构建系统。
Sometimes you just need to get a new feature or product out on the Internet for customers to evaluate. The business may want to beat a competitor, or it may just want to evaluate user reaction to the product. In these scenarios, scalability and reliability are not a problem. So if you feel that building an RPC-based solution meets these requirements, you should look for a good reason not to use it. One reason that it may give you a time-to-market advantage is that more developers are familiar with HTTP than they are with messaging frameworks and concepts. Therefore, you can hire people quicker, and they can build the system quicker.
很难想象有开发人员不懂 HTTP,而那些使用消息平台构建分布式系统的开发人员则更难找到。因此,在招聘人员或组建团队时,如果您构建的是基于 RPC 的系统,那么您可以选择的人才库会更大。如果您选择消息传递,您可能需要培训员工不仅要学会使用消息传递框架,还要学会运用您在本章中学到的基本概念,以便他们能够正确地设计和构建消息传递系统。
It’s hard to think of a developer who doesn’t know HTTP, whereas those building distributed systems with messaging platforms are a little harder to come by. So when you’re hiring people or building a team, you have a bigger pool to choose from if you’re building an RPC-based system. If you go down the messaging route, you may need to train people to not only use messaging frameworks, but use the fundamental concepts you’ve learned about in this chapter so they can design and build messaging systems properly.
使用多种消息传递框架的一大缺点是,它们实际上无法跨不同的开发运行时和操作系统提供紧密的集成。下一章将介绍如何实现这一点。NServiceBus 非常适合集成 .NET 应用程序,但一旦引入其他类型的消息总线,即使是专为 .NET 设计的总线,情况也会变得有些棘手(尽管并非不可能)。不过,有些消息框架确实声称可以处理跨平台场景。因此,如果您在不同平台上运行有界上下文,那么这些框架可能值得研究。
A big drawback to using many messaging frameworks is they don’t really provide tight integration across different development run times and operating systems. You’ll see in the next chapter how NServiceBus is great for integrating. NET applications, but as soon as you introduce another type of message bus, even one that is designed for .NET, life becomes a bit trickier (although certainly not impossible). Some messaging frameworks do claim to handle cross-platform scenarios, though. So they may be worth investigation if you have bounded contexts running on different platforms.
您在电子商务示例中看到,您必须与支付提供商等外部服务进行通信。由于多种原因,这种跨互联网的通信不使用消息传递,而是使用 HTTP。您可能希望其他网站和应用程序与您的系统集成,例如在他们的网站上展示您的产品和服务。为此,您几乎总是会通过 HTTP 提供 REST 或 RPC API。这确保每个人都能够与您的 API 集成,因为每个人都了解并能够使用 HTTP。
You saw in the e-commerce example that you have to communicate with external services like payment providers. This kind of communication across the Internet doesn’t use messaging for many reasons. Instead, it uses HTTP. You may want other websites and applications to integrate with your system, perhaps showing your products and services on their website. To do this, you will nearly always provide REST or RPC APIs over HTTP. This ensures that everybody is able to integrate with your APIs because everyone knows and is capable of working with HTTP.
在上一节中,您了解了在需要可扩展、容错的分布式系统的场景中,响应式架构的必要性。在本节中,您将学习如何运用 SOA 的原则,从有界上下文逐步构建结构化的响应式解决方案。在此过程中,您将了解 SOA 如何引导您组建独立的团队,让他们能够并行开发解决方案,同时仍能实现流畅的运行时集成。
In the previous section, you saw the need to go reactive in scenarios that require scalable, fault-tolerant distributed systems. In this section, you learn how to work down to a structured reactive solution from your bounded contexts using the principles of SOA. In the process, you discover how SOA guides you into having independent teams that can develop their solutions in parallel while still achieving smooth runtime integration.
SOA 的底层需求是隔离代表不同业务功能(例如客户计费或安排发货)的不同软件部分。在真正的 SOA 意义上,服务需要与其他服务松散耦合,并且高度自治——能够在没有其他服务帮助的情况下执行其特定功能。松散耦合的服务提供了许多技术和业务优势。首先,负责这些服务的开发团队可以并行工作,最大限度地减少跨团队干扰。您将在本节后面看到如何实现这一点,但首先,您将了解松散耦合的服务是如何成为从有界上下文过渡到基于响应式原则的集成分布式系统的理想基石。
Underlying SOA is the need to isolate different pieces of software that represent different capabilities of the business, such as billing customers or arranging shipping. In the true SOA sense, services need to be loosely coupled to other services and highly autonomous—able to carry out their specific functionality without help from other services. Loosely coupled services provide a number of technical and business benefits. For a start, the development teams responsible for them can work in parallel with minimal cross-team disruption. You see later in this section how to achieve this but first, you see how loosely coupled services are the ideal stepping stone to go from bounded contexts to integrated distributed systems based on reactive principles.
如果说响应式编程是一套旨在实现松散耦合软件组件的底层技术指南,而 SOA 是一个促进松散耦合业务功能的高层概念,那么这种组合似乎是创建面向业务、可扩展且具有弹性的分布式系统的完美选择。目前缺失的环节是如何将这些优势与 DDD 结合起来。一个答案是将有界上下文视为 SOA 服务,以便将高层有界上下文映射到低层事件驱动的软件组件上。现在,您就有机会获得 SOA 的业务一致性优势以及响应式编程的可扩展性/弹性优势。下一章将提供本节介绍的概念的完整实现示例。
If reactive programming is a set of low-level technical guidelines that lead to loosely coupled software components, and SOA is a high-level concept that facilitates loosely coupled business capabilities, then the combination appears perfect for creating business-oriented, scalable, resilient distributed systems. The missing link so far is how to combine these benefits with DDD. One answer is to view your bounded contexts as SOA services so that you can map high-level bounded contexts onto low-level, event-driven software components. Now you have the potential for the business alignment benefits of SOA and scalability/resiliency benefits of reactive programming. Full examples of implementing the concepts introduced in this section are provided in the next chapter.
在有界上下文中,你可能承担多项职责。例如,在运输有界上下文中,可能存在处理优先订单和标准订单的逻辑。通过将每项主要职责隔离为一个组件,你会发现自己与业务部门的对话更加清晰,并且能够享受到 DDD 的所有其他好处,这些好处你已经看到过,它们都是通过把隐式职责显式化而产生的。此外,通过两个独立的模块,每个模块承担一项职责,你的代码库将更加清晰。这些类型的组件称为业务组件。你可以在图 11.11中看到可能存在于运输有界上下文中的不同业务组件的示例。
Inside a bounded context you may have a number of responsibilities. In a Shipping bounded context, for example, there may be logic to deal with priority orders and standard orders. By isolating each major responsibility as a component, you’ll find yourself having clearer conversations with the business and all the other benefits of DDD that you’ve already seen arise from making the implicit explicit. Additionally, you’ll have increased clarity in your codebase by having two separate modules, each with a single responsibility. These types of components are called business components. You can see examples in Figure 11.11 of the different business components that might exist inside a Shipping bounded context.
图 11.11将运输有界上下文分解为业务组件。
FIGURE 11.11 Decomposing the shipping bounded context into business components.
不要一开始就费力地识别业务组件。相反,要随着时间的推移逐渐将它们区分开来。通常,你会在与领域专家的沟通中发现一些模式,或者在代码中反复检查相同条件的模式。这些都是你深入挖掘并重构模型的触发因素。
Don’t try too hard to identify business components up front. Instead, tease them apart over time. Often you will notice patterns in communication with domain experts, or patterns in code that repeatedly check for the same condition. These are your triggers to dig deeper and restructure your model(s).
一个重要的规则是,SOA 服务只是逻辑容器;一旦您分解了业务组件中的有界上下文,服务本身就没有剩余的工件或行为。正如服务需要解耦且不共享依赖关系一样,同样的规则也适用于业务组件——即使与同一有界上下文内的其他业务组件之间也不存在共享依赖关系。然而,业务组件本身也只是由组件组成的逻辑容器。
One important rule is that SOA services are just logical containers; once you’ve decomposed a bounded context in business components, the service itself has no remaining artifacts or behavior. Just as services need to be decoupled and share no dependencies, the same rule applies to business components—no shared dependencies, even with other business components inside the same bounded context. However, business components themselves are also just logical containers that are composed of components.
业务组件可能负责处理多个事件,因此您应该将它们分解以获得更多好处。例如,在“发货”限界上下文中,“优先发货”业务组件可能处理“OrderPlaced”和“OrderCancelled”消息。通过让这些组件处理这些消息,可以实现更高效的处理。通过由不同的软件组件处理的消息,可以更轻松地根据业务需求更好地调整硬件资源。企业可能希望快速响应OrderPlaced事件,以便尽快安排发货。但是,由于OrderCancelled消息数量很少,因此快速响应这些消息对他们来说并不那么重要。因此,如果将“优先发货”业务组件分解为“安排发货”组件和“取消发货”组件,企业可以将“安排发货”组件放在速度极快的裸机服务器上,同时让“取消发货”组件在小型虚拟化服务器上缓慢运行。图 11.12显示了如何将“发货”限界上下文内的业务组件分解为组件的示例。
Business components may be responsible for handling multiple events, so you should break them down for further benefits. For example, in the Shipping bounded context, the Priority Shipping business component may handle OrderPlaced and OrderCancelled messages. By having these messages processed by different software components, it’s easier to better align hardware resources with the needs of the business. The business may want a rapid response to the OrderPlaced event so that shipping is arranged as soon as possible. However, it’s not so important for them to respond as quickly to OrderCancelled messages because there are so few of them. Therefore, if the Priority Shipping business component is broken down into the Arrange Shipping component and the Cancel Shipping component, the business can put the Arrange Shipping component on blazing-fast, bare-metal servers while allowing the Cancel Shipping component to slowly grind away on a small virtualized server. Figure 11.12 shows an example of how the business components inside a Shipping bounded context could be broken down into components.
图 11.12运输有界上下文分解为业务组件和组件。
FIGURE 11.12 Shipping bounded context broken down into business components and components.
最终,组件的优势在于,企业可以明智地将资金投入到系统中可能带来收入增长的特定部分。然而,硬件经济性只是其中一项优势。另一个优势是,可以根据业务优先级和性能需求将组件部署到不同的网络上。因此,一些需要高性能的有界上下文可以拥有极快的服务器和专用的高带宽网络。通常,使用细粒度的组件可以更灵活地满足业务需求。
Ultimately, the benefit of components is that the business can spend money intelligently on the specific parts of the system that are likely to result in increased revenue. Hardware economics is just one of the benefits, though. Another is the possibility of locating components on different networks in line with business priority and performance needs. So, some bounded contexts that need high performance can have blazing-fast servers and dedicated high-bandwidth networks. In general, by having granular components you have more flexibility to align with the needs of the business.
您可能已经意识到,组件是部署的单位。图 11.13直观地展示了如何在具有不同资源级别(CPU、RAM、SSD 等)的不同机器或云实例上部署电子商务应用程序中的不同组件。
As you may have gathered, components are the unit of deployment. Figure 11.13 visualizes this by showing how you can deploy different components in an e-commerce application on different machines or cloud instances with varying resource levels (CPU, RAM, SSDs, and so on).
图 11.13运输有界上下文中组件的可能部署视图。
FIGURE 11.13 Possible deployment view of components in the Shipping bounded context.
虽然有界上下文和业务组件之间差异很大,以至于它们之间的 RPC 调用或共享数据库可能会导致弹性或可扩展性问题,但组件之间却不会出现同样的问题。组件之间通常协作非常紧密;因此,拥有像数据库这样的共享依赖关系可以提高内聚性,而不一定会导致问题。不过,如果您认为在特定情况下使用消息传递能带来最佳的平衡,您仍然可以自由地在组件之间使用消息传递。
Whereas bounded contexts and business components are distinct enough that having RPC calls between them or a shared database can lead to resilience or scalability problems, the same is not true for components. Components often work together very closely; therefore, having shared dependencies like a database increases cohesion without necessarily causing problems. You are still free to use messaging between components if you feel it gives the best trade off in a given situation, though.
您可以使用图 11.14来帮助您记住共享依赖关系可能仅存在于业务组件内部,而不存在于它们之间。
You can use Figure 11.14 to help you remember that shared dependencies may exist only inside a business component, not between them.
图 11.14仅允许在同一业务组件内的组件之间共享依赖关系。
FIGURE 11.14 Sharing dependencies is only allowed between components inside the same business component.
与软件开发中的许多其他术语一样,“组件”这个术语含义模糊且含糊不清。在本章中,您可以看到有业务组件和组件(不带前缀)。遗憾的是,社区尚未真正为本章中提到的“组件”提出一个标准名称。Udi Dahan 将它们称为自治组件(http://www.drdobbs.com/web-development/business-and-autonomous-components-in-so/192200219 )。其他人根据基于组件的开发定义(http://en.wikipedia.org/wiki/Component-based_software_engineering ),将它们称为组件(如本章中所示) 。然而,现在其他人开始将它们称为微服务,如下一节所示。
The term component, like many others in software development, is vague and ambiguous. In this chapter, you can see that there are business components and components (without a prefix). Unfortunately, the community hasn’t really come up with a standard name for what this chapter refers to as just components. Udi Dahan refers to them as autonomous components (http://www.drdobbs.com/web-development/business-and-autonomous-components-in-so/192200219). Others refer to them as components (as in this chapter) based on the component-based development definition (http://en.wikipedia.org/wiki/Component-based _ software _ engineering). Yet others are now starting to refer to them as micro services, as shown in the next section.
Netflix 采用了一种细粒度的 SOA 方法,其他公司现在也开始以微服务架构 (MSA) 的名义应用这种方法。MSA 最大的优势在于其上市时间和实验优势。所以,如果你正在为一家企业工作如果您喜欢对其系统进行大量更改并衡量每个更改的影响以推动新功能和现有功能的发展,那么您应该考虑 MSA。
Netflix uses a fine-grained approach to SOA that other companies are now starting to apply under the name Micro Service Architecture (MSA). Some of the biggest reported benefits of MSA are its time-to-market and experimentation advantages. So if you find yourself working for a business that likes to make lots of changes to its system and measure the impact of every change to drive the evolution of new and existing features, you should consider MSA.
到目前为止,遵循本章中的指导,您将接近 MSA,其中每个组件类似于一个微服务。但是,MSA 有一些明显的区别。每个微服务必须完全自治,以便可以添加、删除或修改业务功能而不会影响任何其他微服务。因此,每个微服务都应该有自己的宕机数据库,并且几乎肯定会通过发布/订阅使用事件进行通信,因为命令和 RPC 会引入耦合。至少,这是 Fred George(最早开始谈论 MSA 的人之一)在其 2012 年 Oredev 演讲(https://www.youtube.com/watch?v=q3q6ZsjZ_f0)中描述的 MSA 风格。微服务的另一个特性是它们的代码量应该少于一千行,尽管这肯定存在争议,但许多人都认同这一点。
Following the guidance in this chapter so far will get you close to an MSA, where each component is akin to a micro service. However, MSA has some distinct differences. Each micro service must be completely autonomous so that the business feature can be added, removed, or modified without impacting any other micro service. Therefore, each micro service should have its down database and, almost certainly, communicate using events via publish/subscribe because commands and RPC introduce coupling. At least, this is the style of MSA described by Fred George, one of the first people to start talking about MSA, in his Oredev 2012 talk (https://www.youtube.com/watch?v=q3q6ZsjZ_f0). Another feature of micro services that many people agree on, although it is certainly contentious, is that they should be less than a thousand lines of code.
有关 MSA 的更多信息,Martin Fowler 的介绍性博客系列是一个很好的起点(http://martinfowler.com/articles/microservices.html)。
For more information on MSA, Martin Fowler’s introductory blog series is a useful place to begin (http://martinfowler.com/articles/microservices.html).
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 12 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 12 download and individually named according to the names throughout the chapter.
希望上一章中的概念能够让你对构建可扩展分布式系统的前景感到兴奋,同时还能充分享受领域驱动设计 (DDD) 的优势。本章旨在为你提供所需的基础技能,以便你能够立即将这些概念应用到实际系统中。为了掌握必要的初始技能:您将使用 NServiceBus 和 Mass Transit 构建一个电子商务消息传递应用程序。在此过程中,您将学习如何将从领域专家那里获得的知识融入到您的模型中,并运用一些技巧在代码中明确领域概念。您将看到,以问题领域中发生的事件命名消息是实现此目的的一种特别有效的技巧。
Hopefully the concepts in the previous chapter got you excited about the prospect of building scalable distributed systems while still getting the full benefits of Domain-Driven Design (DDD). This chapter aims to provide you with the foundational skills you need so that you can immediately begin to apply those concepts to real systems. To acquire the necessary initial skills, you’re going to build an e-commerce messaging application using NServiceBus and Mass Transit. Along the way, you will see how to incorporate the knowledge you gain from domain experts into your model by using techniques that make domain concepts explicit in the code. You see how naming your messages after events that occur in the problem domain is one especially good technique for achieving this.
本章中的示例重点展示您可能会遇到的常见场景。其中一些场景您可能在第 11 章“有界上下文集成简介”中已经熟悉,例如,为那些访问您无法控制的服务的同步超文本传输协议 (HTTP) 调用添加可靠性。然而,构建系统只是整个难题的一小部分,因为系统需要维护。因此,在本章中,您还将学习如何处理影响多个团队的消息格式更改,如何监控消息系统的性能和错误,以及如何随着业务需求的增长扩展到多台机器。
Throughout this chapter, the examples focus on showing common scenarios you are likely to come across. Some of these you will be familiar with from Chapter 11, “ Introduction to Bounded Context Integration,” such as adding reliability to synchronous Hypertext Transport Protocol (HTTP) calls that hit services you don’t control. Building a system is only a small part of the puzzle, though, because systems need to be maintained. So in this chapter you’ll also learn how to deal with changes to message formats that affect multiple teams, how to monitor the performance and errors of your messaging system, and how to scale out to multiple machines as business needs increase.
松散耦合系统的一大优势在于,您可以拥有独立的团队,从而实现高效的开发,正如上一章所述。在本章中,您将在实现层面了解如何组织源文件和项目以实现这一点。在深入到这一层面之前,通常有必要设计一个系统,以便每个团队都了解其有界上下文如何融入整体架构。因此,在本章中,您将学习如何创建架构图,以便可视化重要决策并捕获领域驱动的用例。
One of the big benefits of having loosely coupled systems is that you can have independent teams that can achieve high development throughout, as discussed in the previous chapter. In this chapter, you will see at the implementation level how you can organize your source files and projects to enable this. Before you get down to that level, it’s often useful to design a system so that each team understands how its bounded contexts fit into the big picture. So in this chapter, you will learn how to create architecture diagrams that allow you to visualize important decisions and capture your domain-driven use-cases.
消息传递系统确实存在一些缺点,本章将演示如何应对其中的一些问题。与 HTTP 调用相比,消息传递系统的一个缺点是缺乏标准化的格式。如果您想要集成两个使用不同消息传递框架的解决方案,这可能会成为一个问题。然而,使用一种名为消息桥的消息传递模式可以部分解决这个问题。在本章中,您将构建一个消息桥,将 NServiceBus 消息传递系统连接到 Mass Transit 消息传递系统。
Messaging systems do have drawbacks, and this chapter demonstrates how to deal with some of them. One drawback compared to HTTP calls is that there is no standardization of formats. This can be a problem if you want to integrate two solutions using different messaging frameworks. However, this is a partially solved problem by using a messaging pattern called the messaging bridge. In this chapter, you will build a messaging bridge that connects an NServiceBus messaging system to a Mass Transit messaging system.
在开始编写代码之前,您需要了解一些有关消息传递系统的细节。这些细节将极大地提升您在完成示例代码时对所构建内容的理解能力。本章接下来将讨论这些内容。
Before you begin writing code, there are some details about messaging systems that you need to learn about. These will greatly improve your ability to understand what you are building as you work through the examples. This chapter discusses those next.
消息传递应用程序与传统的非分布式面向对象应用程序有着根本的不同。虽然它们带来了容错性和可扩展性等新优势,但也带来了新的挑战,例如异步编程模型需要截然不同的思维方式。然而,除了上一章介绍的理论之外,本节介绍的消息传递基础知识应该能为您提供使用异步消息传递构建响应式应用程序所需的所有基础知识。您需要了解的第一个概念是消息总线,它是将系统连接在一起的粘合剂。
Messaging applications are fundamentally different from traditional, nondistributed object-oriented applications. Although they bring new benefits such as fault tolerance and scalability, they also bring new challenges, such as an asynchronous programming model that requires a significantly different mind-set. However, in addition to the theory presented in the previous chapter, the messaging fundamentals introduced in this section should give you all the foundational knowledge you need to begin building reactive applications using asynchronous messaging. The first concept you’ll need to learn about is the message bus, which is the glue that holds the system together.
如果系统中只有一个中心化组件负责传输所有消息,那么一旦它停止工作,整个系统就会崩溃。同样令人担忧的是,这个单一组件这会使系统的各个部分更难根据业务需求进行扩展。您可以使用消息总线(一种分布式系统,在每个发送或接收消息的组件上运行代理)来解决这些问题,从而避免集中式单点故障。消息总线是所有这些组件协同工作的集合,如图12.1所示。
If one centralized component in the system was responsible for transmitting all the messages, the whole system would collapse if it stopped working. Equally as concerning, this single component would make it more difficult for individual parts of the system to be scaled according to business need. You can solve these problems by using a message bus—a distributed system that has agents running on every component that sends or receives messages—avoiding the need for a centralized single point of failure. A message bus is all of these pieces collectively working together, as shown in Figure 12.1.
图 12.1消息总线架构。
FIGURE 12.1 Message bus architecture.
使用消息总线可以将有界上下文完全解耦。每个有界上下文(更具体地说,每个组件)仅连接到总线。如果一个组件离线,其他连接的组件都不会受到影响。
Using a message bus allows bounded contexts to be completely decoupled from each other. Each bounded context—more specifically, each component—is connected only to the bus. If a component goes offline, none of the other connected components is affected.
如果无法保证消息一定能到达,那么从一个限界上下文向另一个限界上下文发送消息的成本可能会很高——如果由于消息在系统中某个地方丢失而导致客户从信用卡中扣款却无法发货,他们会非常不高兴。这就是需要可靠消息传递的原因。遗憾的是,几乎不可能保证一条消息始终只传递一次。如果消息已发送但未收到确认,则可以再次发送。但是,如果收到了消息但未收到确认怎么办?当然,系统会错误地假设未收到第一条消息,从而再次发送同一条消息。
Sending messages from one of your bounded contexts to another could be costly if there were no guarantees the message would ever get there—customers aren’t very happy when you charge their credit card but fail to ship their product because a message got lost in your system somewhere. This is the reason that reliable messaging is needed. Unfortunately, it’s almost impossible to guarantee that a message will always be delivered only once. If a message is sent and no acknowledgement is received, it can be sent again. But what if the message was received but the acknowledgement wasn’t? Of course, the same message will be sent again under the incorrect assumption that the first was not received.
由于可靠传递的挑战,存在各种可靠的消息传递模式,每种模式都有自己的挑战和权衡,包括:如前所述,至少一次、最多一次和仅一次传递。
Because of the challenges with reliable delivery, there are a variety of reliable messaging patterns, each with its own challenges and trade-offs, including: at-least-once, at-most-once, and only-once delivery as previously alluded to.
随着你对消息传递系统的了解,你会发现“至少一次”通常是首选方案,即使它仍然存在消息可能被处理两次的风险。为了避免这种情况可能造成的问题,例如对一个订单收取两次重要客户的费用,本章将介绍如何将“至少一次”投递与幂等消息相结合。
As you learn about messaging systems, you’ll see that at-least-once is normally the preferred option, even though it does still carry the risk that a message may be handled twice. To avoid problems that this may cause, like charging valuable customers twice for a single order, this chapter shows how to combine at-least-once-delivery with idempotent messages.
至少一次投递涉及重试失败或未收到接收方确认(看似失败)的消息。重试失败或未收到确认的消息的实际流程依赖于一种称为“存储转发”的模式。
At-least-once delivery involves retrying messages that failed or no acknowledgement from the receiver was received (appearing to fail). The actual plumbing for retrying failed or unacknowledged messages relies on a pattern known as store-and-forward.
消息发送后,可能无法到达接收者。网络、硬件或编程错误都可能导致这种情况。存储转发模式通过在发送消息之前存储消息来解决许多此类问题。如果消息到达接收者并得到确认,则本地副本将被删除。但是,如果消息未到达接收者,则会再次尝试发送。在本章中,您将看到消息传递框架如何处理大部分复杂性。通过允许您设置有关重试消息的频率以及两次尝试之间等待的时间的规则。
When a message is sent, it might not reach the recipient. The network could fail, hardware could fail, or programming errors could manifest. The store-and-forward pattern remedies many such problems by storing the message before it is sent. If the message reaches the recipient and is acknowledged, the local copy is deleted. However, if the message does not reach the recipient, it is tried again. You’ll see in this chapter how messaging frameworks deal with most of the complexity by allowing you to set up rules about how often messages should be retried and how long to wait between attempts.
大多数消息传递框架使用队列来存储消息。因此,当服务 A 向服务 B 发送消息时,该消息最终会进入服务 B 的队列。当服务 B 处理该消息时,它会将其从队列中移除。但是,如果服务 B 未完成消息处理,该消息也会被放回队列并稍后重试。
Most messaging frameworks use queues to store messages. So when Service A sends Service B a message, the message ends up on Service B’s queue. When Service B processes the message, it takes it off the queue. However, if Service B doesn’t complete processing the message, it also gets put back on the queue and retried later.
有时你会想要发送指定需要发生某事的消息,例如第 11 章中的PlaceOrder。这类消息称为命令。使用命令时,发送者和接收者之间存在逻辑耦合,因为发送者知道接收者应该如何处理消息。命令只能由一个接收者处理,这非常不灵活。或者,事件发出某些事情发生的信号,例如OrderCreated。事件比命令更松散地耦合,因为发送者不知道接收者将如何处理消息。事实上,发送者不知道谁处理消息。这是因为事件基于发布/订阅模式。
Sometimes you will want to send messages that specify something needs to happen, such as PlaceOrder from Chapter 11. These kinds of messages are known as commands. With commands there is a logical coupling between the sender and the receiver because the sender knows how the receiver should handle the message. Commands are only handled by one receiver, which is quite inflexible. Alternatively, events signal that something happened, such as OrderCreated. Events are more loosely coupled than commands because the sender doesn’t know how the receiver will handle the message. In fact, the sender doesn’t know who handles the messages. This is because events are based on the publish/subscribe pattern.
发布/订阅提供的松散耦合意味着事件通常比命令更受欢迎。事件的一个显著优势是,您可以在不更改现有代码的情况下添加新的订阅者。这使得企业能够添加全新的有界上下文,而无需更改任何现有代码或减慢其他开发团队的速度。在上一章的电子商务示例中,只需订阅OrderCreated事件即可将新的 Marketing 有界上下文添加到流程中,如图 12.2所示。
The looser coupling offered by publish/subscribe means events are often preferable to commands. One significant advantage of events is that you can add new subscribers without changing existing code. This allows the business to add a completely new bounded context without changing any existing code or slowing down other development teams. In the e-commerce example from the previous chapter, the new Marketing bounded context could be added to the flow just by subscribing to the OrderCreated event, as illustrated in Figure 12.2.
图 12.2添加新的事件订阅者不会影响现有代码。
FIGURE 12.2 Adding new event subscribers doesn’t affect existing code.
您可能会在网上遇到一些关于命令和事件命名的争论。您可以自由选择,但最常见的意见是将命令命名为您希望发生的指令(例如 PlaceOrder、ChangeAddress、RefundAccount ),并将事件命名为描述已发生事件的过去时(例如OrderCreated、MovedAddress、AccountRefunded )。
You might run into some debate online about naming commands and events. You’re free to make your own choices, but the most common opinion is to name commands as instructions that you want to happen—PlaceOrder, ChangeAddress, RefundAccount—and to name events in the past tense describing what has happened—OrderCreated, MovedAddress, AccountRefunded.
如果您构建的消息传递系统中的每个有界上下文都有自己的数据库,那么您最终会遇到在使用大事务的系统中不常见的情况。一个常见的例子是没有付款确认的订单。在使用事务的系统中,这种状态可能无法实现,因为创建订单是处理付款的同一原子操作的一部分。如果付款失败,则不会创建任何订单。相反,消息传递解决方案允许这种状态,因为它们消除了大事务并实现了最终一致性。
If you’re building messaging systems in which each bounded context has its own database, you will end up in situations that aren’t common in systems that use big transactions. An order without payment confirmation is a common example. In a system that uses transactions, this state might not be possible because creating an order is part of the same atomic operation that processes payments. Should the payment fail, no order will be created. Conversely, messaging solutions would allow this state because they remove big transactions and are eventually consistent.
最终一致性系统最重要的方面之一是用户体验管理。在一致性系统中,用户会等到整个交易完成。此时,他们知道订单已创建、付款已处理、发货已安排。然而,正如第 11 章所述,大型交易的扩展性并不总是很好。在最终一致性系统中,用户通常会立即收到订单意向的确认,并在付款和发货处理完毕并安排好后再收到电子邮件确认,这看起来可能会降低用户体验。
One of the most important aspects of eventually consistent systems is managing the user experience. In consistent systems, users wait until the whole transaction completes. At which point, they know the order was created, the payment was processed, and the shipping was arranged. However, you know from Chapter 11 that big transactions do not always scale very well. In eventually consistent systems, users often get an immediate confirmation that the intent to place an order has been received, with a further e-mail confirmation later when payment and shipping have been processed and arranged, which can appear to be a degradation of user experience.
您可以将最终一致性发挥到极致,转向业务,思考在最终一致状态下应该发生什么。有时,您可以挖掘隐藏的业务策略或机会。如需更详细的阅读,强烈推荐 Udi Dahan 的博客文章“竞争条件不存在”( http://www.udidahan.com/2010/08/31/race-conditions-dont-exist/ )。
You can use eventual consistency as a positive by turning to the business and asking what should happen in eventually consistent states. Sometimes you can unlock hidden business policies or opportunities. For more detailed reading, Udi Dahan’s “Race Conditions Don’t Exist” blog post is highly recommended (http://www.udidahan.com/2010/08/31/race-conditions-dont-exist/).
现在是您积累实践经验构建响应式消息系统的机会。首先,您将构建一个电子商务系统,让用户可以在线购买自己喜欢的商品。您将使用命令、事件和其他常见的消息传递模式,了解消息传递系统如何提供增强的可扩展性和可靠性。准备就绪后,您的首要任务是下载 NServiceBus,以便您的计算机配备必要的依赖项来运行示例。
Now is your opportunity to get practical experience building a reactive messaging system. To begin with, you’ll build an e-commerce system where users can make online purchases of their favorite products. You will use commands, events, and other well-known messaging patterns to appreciate how messaging systems provide the platform for enhanced scalability and reliability. When you’re ready, your first task is to download NServiceBus so that your machine is equipped with the necessary dependencies to work through the examples.
NServiceBus 的入门并不难;只需下载并运行安装程序即可。为了继续本章,您需要选择 4.3.3 版本(https://github.com/Particular/NServiceBus/releases/download/4.3.3/Particular.NServiceBus-4.3.3.exe)。这会将所需的依赖项添加到您的操作系统,包括 Microsoft 消息队列 (MSMQ) 和分布式事务处理协调器 (DTC)。您很快就会看到,您仍然需要在项目中引用 NServiceBus 程序集。
It’s not too difficult to get started with NServiceBus; simply download and run the installer. To follow along with this chapter, you need to choose version 4.3.3 (https://github.com/Particular/NServiceBus/releases/download/4.3.3/Particular.NServiceBus-4.3.3.exe). This adds the required dependencies to your operating system, including Microsoft Message Queuing (MSMQ) and the Distributed Transactions Coordinator (DTC). As you will see shortly, you still need to reference the NServiceBus assemblies in your projects.
在打开 IDE 之前,先将要构建的内容可视化会很有帮助。当一些概念比较新时,这一点尤其有用。了解它们如何组合在一起,可以让你更好地理解实际构建的内容。本章中的电商应用程序设计需要三个设计步骤。第一步是创建一个容器图,展示应用程序分组、技术选择和通信协议。第二步是创建一个组件图,展示有界上下文之间的逻辑流。但第一步也是最重要的一步是从领域开始。
Before you crack open an IDE, it can be helpful to visualize what you are trying to build. This can be particularly useful when some of the concepts are new. Seeing how they fit together allows you to better understand what you’re actually building as you go along. Designing the e-commerce application in this chapter requires three design steps. One step is to create a containers diagram showing application groupings, technology choices, and communication protocols. Another step is to create a component diagram showing the flow of logic between bounded contexts. But the first and most important step is to start with the domain.
正如您在本书第一部分所学到的,构建软件最重要的方面无疑是理解构建它的原因,这样您才能为系统的目标用户提供价值。因此,在设计系统时,强烈建议您花费大量时间与领域专家一起构建一种通用语言 (UL),并将隐含的领域概念明确化。一些领域驱动设计 (DDD) 从业者建议您首先识别领域中发生的重要事件。这被称为事件风暴 (Event Storming ),它可以与消息传递应用程序高度协同 ( http://ziobrando.blogspot.co.uk/2013/11/introducing-event-storming.html )。
As you’ve learned in the first part of this book, arguably the most important aspect to building software is to understand why you are building it so that you can provide value to people you are building the system for. So when designing a system, it is highly recommended that you spend a significant amount of time with domain experts building up a ubiquitous language (UL) and making implicit domain concepts explicit. Several DDD practitioners recommend that you start by identifying important events that occur in the domain. This is known as Event Storming, and it can be highly synergistic with messaging applications (http://ziobrando.blogspot.co.uk/2013/11/introducing-event-storming.html).
当你构建通过共享命令和事件进行集成的有界上下文时,你就有绝佳的机会将领域中发生的重要事件与软件系统中发生的事件进行映射。许多知名的 DDD 从业者都使用此模式来表达在构建的消息传递系统中明确地体现领域概念。要做到这一点,首先需要识别现实世界领域中发生的重要事件。这些事件被称为领域事件。
When you’re building bounded contexts that integrate by sharing commands and events, you have an excellent opportunity to map important events that occur in the domain with the events occurring in the software system. This pattern is used by a number of well-known DDD practitioners to express domain concepts explicitly in the messaging systems they build. To do this yourself, you first need to identify important events that occur in the real-world domain. These are known as domain events.
在本书第一部分“领域驱动设计的原则与实践”中,你了解了多种捕获领域知识的方法,包括事件。这通常发生在你与领域专家进行知识研讨、非正式聚会,甚至团队午餐等场合。在上一章中,我们介绍了一些在现实世界领域中发生的事件,即使没有软件系统,这些事件仍然会发生。其中一些事件包括“下订单”、“付款已接受”和“安排发货”。在构建消息传递系统时,如果你额外努力寻找类似的领域事件,将会获得丰厚的回报。
In Part I of this book, “The Principles and Practices of Domain-Driven Design,” you saw a number of ways to capture domain knowledge, including events. This normally occurs when you are with domain experts during knowledge-crunching sessions, informal get-togethers, or even team lunches and such. In the previous chapter, a number of events occurred in the real-world domain and would still occur even if there was no software system. Some of them are Order Placed, Payment Accepted, and Shipping Arranged. When you are building a messaging system, it can pay dividends if you make an extra effort to look for domain events like these.
捕获领域事件后,它们将成为 UL 的一部分,您可以开始将它们拼凑在一起,了解它们如何组合成完整的业务用例。存储和共享这些知识的一个好方法是在组件图上可视化事件序列。
Once captured, your domain events form part of the UL, and you can start to piece them together to learn how they combine to form full business use cases. An excellent way to store and share this knowledge is by visualizing the sequence of events on a component diagram.
在编写代码之前,先勾勒出高层逻辑,这样您就可以更高效、更正确地构建组件,因为您了解自己在做什么。这正是组件图大有裨益的地方。开始创建组件图的最佳时机是与领域专家进行知识研讨时。您可以一起绘制基本草图,仅使用方框和线条来传达领域事件和流程。当您开始编码时,您已经对需要构建的内容以及需要在系统中建模的 UL 中的术语有了大致的了解。
By sketching out high-level logic before you write code, you are in a better position to build the component efficiently and properly because you understand what you are doing. This is where a component diagram provides a lot of benefit. A useful time to start creating component diagrams is during knowledge-crunching sessions with domain experts. You can produce basic sketches together using just boxes and lines to communicate domain events and processes. When you then sit down to start coding, you already have an idea of what you need to build and terminology from the UL that needs to be modeled in your system.
组件图没有正式的结构;它们传达的是特定组件之间的逻辑流程或交互。不应过于深入地描述技术选择,也不应过于详细地展示类或方法名称。图 12.3中展示了一个示例,它展示了即将构建的电子商务应用程序中有界上下文之间的消息流。
Component diagrams have no formal structure; they communicate the flow of logic or interaction between certain components. You shouldn’t go too high level and show technology choices, and you shouldn’t go too detailed and show class or method names. You can see an example of this in Figure 12.3, which shows the flow of messages between bounded contexts in the e-commerce application you’re going to build.
图 12.3显示域事件的组件图。
FIGURE 12.3 A component diagram showing domain events.
您可以根据需要绘制任意数量的组件图。在本例中,有一个组件图用于描述订单下单场景的流程。您可以先遵循以下惯例:每个高级用例对应一个组件图,并评估其效果。
You can have as many component diagrams as you feel are necessary. In this example, there is one component diagram that communicates the flow of the order placement scenario. You could start by following this convention: having one component diagram per high-level use case and assessing how that works for you.
一旦你与领域专家共事一段时间,并开始理解该领域,那么在某个时候,你就需要开始构建系统了。此时,你需要将业务需求映射到一个能够为其提供价值的分布式软件系统上。创建容器图是一种可以帮助你平衡功能性领域需求和非功能性技术需求的技术。
Once you have spent time with domain experts and you are starting to understand the domain, at some point you’ll need to start building the system. It’s at this time you need to map the requirements of the business onto a working, distributed software system that provides them value. One technique that can help you balance the functional domain requirements with the nonfunctional technical requirements is to create containers diagrams.
应用程序的不同部分如何相互通信?如何确保系统能够提供必要的容错能力和可扩展性?如何确保团队中的每个人都了解正在构建的内容,以免犯下大错?对于大多数软件项目来说,这些都是重要的问题,而容器图可以帮助您解答这些问题。这种类型的图展示了系统不同部分如何分组、如何通信以及主要的技术选择。
How do the different parts of an application talk to each other? How can you be confident the system will provide the necessary fault tolerance and scalability? How can you make sure everyone on the team understands what is being built so they don’t do something completely wrong? These are important questions for most software projects, and a containers diagram helps you answer them. This type of diagram shows how different parts of the system are grouped, how different parts of the system communicate, and what the major technology choices are.
请看图 12.4,这是本章重点介绍的电子商务应用程序的容器图。
Take a look at Figure 12.4, which is a containers diagram of the e-commerce application that is the focus of this chapter.
图 12.4典型电子商务应用程序的容器图。
FIGURE 12.4 A containers diagram for a typical e-commerce application.
系统设计中需要注意的一些关键点如下:
Some of the key points to take note of in the system design follow:
从图 12.4中的容器图可以看出,通过使用消息传递解决了容错和可扩展性问题。您还可以看到,每个有界上下文都提供了一个 API,浏览器可以使用该 API 获取数据,并通过 HTTP 显示在网页上。该图还验证了一些技术选择。例如,由于每个有界上下文都使用 C#,因此您可以使用单一的消息传递技术 (NServiceBus),从而简化集成。但是,如果您的要求指出可以使用不同的语言和平台构建新的有界上下文,该设计可能会让您更深入地考虑选择 NServiceBus 或任何消息传递技术,而更喜欢 REST(您将在下一章中使用它构建系统),因为它具有更友好的跨平台集成。
From the containers diagram in Figure 12.4, you can see that fault tolerance and scalability have been addressed by using messaging. You can also see that each bounded context provides an API that the browser can use to get data to show on web pages using HTTP. The diagram also validates some technology choices. For instance, because each bounded context is using C#, you can use a single messaging technology (NServiceBus) that makes integration easier. However, if your requirements stated that new bounded contexts may be built using different languages and platforms, the design might make you think deeper about choosing NServiceBus or any messaging technology and instead prefer REST (which you will build a system with in the next chapter) for its friendlier cross-platform integration.
根据上一章的讨论,有些设计决策可能比较合理。其他一些决策你可能觉得违反直觉,或者只是不同意。这完全没问题。读完本章后,你将涵盖此应用程序中的所有设计决策。你可以自行决定是否同意。
Some of the design decisions may make sense following the discussion from the previous chapter. Other decisions you may find counterintuitive or just disagree with. And that’s completely fine. By the end of this chapter, all the design decisions in this application will have been covered. You can then decide for yourself if you agree.
通过与领域专家的频繁合作,您将不断深入了解该领域。您已经意识到,随着学习过程的推进,您需要重构领域模型并增强 UL。因此,您的系统设计也需要更新以反映您的学习成果。如果领域中增加了新的事件,或者您的上下文边界需要调整,您应该尝试围绕新的发现来构建架构模型,并相应地更新图表。这样做之后,您的图表将保持相关性,帮助人们做出明智的业务决策,并更好地理解他们正在构建的内容。您的架构也将与问题领域更加契合。
Through frequent collaboration with domain experts, you continue to learn more about the domain. You’ve seen that you need to refactor your domain models and enhance the UL as the learning process continues. Accordingly, the design of your system needs to be updated to reflect your learnings. If new events are added to the domain or your context boundaries need to be adjusted, you should try to model your architecture around your new findings and update your diagrams accordingly. When you do this, your diagrams continue to be relevant, help people make informed business decisions, and better understand what they are building. Your architecture will also be more closely aligned with the problem domain.
下单始于客户在网站上结账。这是图 12.3所示组件图中的第一步。要启动消息传递系统,您需要创建一个 Web 应用程序,该应用程序可以通过发送代表客户意愿的PlaceOrder命令来启动此流程。本示例将使用一个简单的 MCV 4 网站,该网站只有一个可以下单的页面。图 12.5展示了它的外观。
Placing an order begins with customers checking out on the website. This is the first step in the component diagram shown in Figure 12.3. To get started with the messaging system, you need to create a web application that can initiate this process by sending a PlaceOrder command representing the customer’s wishes. For this example, a simple MCV 4 website with just a single page where orders can be placed will be used. Figure 12.5 shows what this will look like.
图 12.5 下订单的基本网页。
FIGURE 12.5 The basic web page for placing an order.
正如你所见,UI 的外观设计并不是本章的重点。然而,将数据推送到消息系统却是重点。
As you can clearly see, the appearance of the UI is not a significant part of this chapter. However, pushing data into the messaging system is.
在创建 Web 应用程序之前,您需要在 Visual Studio 中创建一个名为DDDesign的空白解决方案。该解决方案最终将托管整个系统。图 12.6演示了创建空 Visual Studio 解决方案的过程。
Before creating the web application, you need to create a blank solution inside Visual Studio called DDDesign. This solution will eventually host the entire system. Figure 12.6 illustrates the process of creating an empty Visual Studio solution.
图 12.6创建一个空的 Visual Studio 解决方案。
FIGURE 12.6 Creating an empty Visual Studio solution.
在 NServiceBus 应用中,消息是限界上下文之间的契约。消息必须能够被发布它们的限界上下文和消费它们的限界上下文访问。因此,一个好的惯例是,每个限界上下文都有一个项目,只包含它发布的消息。其他限界上下文可以引用这个项目来访问消息。
In an NServiceBus application, messages are the contract between bounded contexts. The messages must be accessible to the bounded context that publishes them and the bounded contexts that consume them. So a good convention is for each bounded context to have a project that contains just the messages it publishes. Other bounded contexts can reference this project to access the messages.
图 12.3中的组件图显示,第一步是从网站向销售限界上下文发送PlaceOrder命令。这使得PlaceOrder命令成为属于销售边界上下文的消息。因此,您需要创建的第一个项目是销售边界上下文的消息项目。您可以通过在刚刚创建的DDDesign解决方案中添加一个名为Sales.Messages的新 C# 类库来实现。
The component diagram in Figure 12.3 shows that the first step is a PlaceOrder command being sent from the website to the Sales bounded context. This makes the PlaceOrder command a message that belongs to the Sales bounded context. Therefore, the first project you need to create is the Sales bounded context’s messages project. You can do that by adding a new C# class library called Sales.Messages to the DDDesign solution you just created.
在新创建的Sales.Messages项目中,现在可以添加PlaceOrder命令,该命令代表用户想要购买某些产品的真实场景。在本例中,在项目根目录中添加一个名为Commands的文件夹,并添加一个名为PlaceOrder的类,其内容如下:
Inside your newly created Sales.Messages project, you can now add the PlaceOrder command that represents the real-world scenario of a user indicating he would like to buy certain products. For this example, add a folder called Commands to the root of the project and a class called PlaceOrder with the following content:
namespace Sales.Messages.Commands
{
public class PlaceOrder
{
public string UserId { get; set; }
public string[] ProductIds { get; set; }
public string ShippingTypeId { get; set;}
public DateTime TimeStamp { get; set; }
}
}
namespace Sales.Messages.Commands
{
public class PlaceOrder
{
public string UserId { get; set; }
public string[] ProductIds { get; set; }
public string ShippingTypeId { get; set;}
public DateTime TimeStamp { get; set; }
}
}
现在,您可以创建可以发送PlaceOrder命令的网站了。在同一个解决方案中,您需要添加一个新的 ASP.NET MVC 4 项目,选择空模板和 Razor 视图引擎。将该项目命名为DDDesign.Web 。这些步骤如图 12.7和图 12.8所示。
Now you’re in a position to create the website that can send the PlaceOrder command. Inside the same solution, you need to add a new ASP.NET MVC 4 project, choosing the empty template and the Razor View Engine. Name this project DDDesign.Web. These steps are illustrated in Figure 12.7 and Figure 12.8.
图 12.7添加 DDDesign.Web MVC 4 Web 应用程序。
FIGURE 12.7 Adding the DDDesign.Web MVC 4 web application.
图 12.8使用 Razor 视图引擎选择空的 ASP.NET MVC 模板。
FIGURE 12.8 Selecting empty ASP.NET MVC template with the Razor view engine.
现在,您需要在解决方案资源管理器中右键单击DDDesign.Web项目图标,并选择“添加引用”。然后选择“解决方案项目”选项。最后,您可以添加对Sales.Messages
项目的引用,如图 12.9所示。
Now you need to right-click on the DDDesign.Web project icon in the Solution Explorer and choose Add Reference. Then select the Solution Projects option. Finally, you can add a reference to the Sales.Messages project, as illustrated in Figure 12.9.
图 12.9从 DDDesign.Web 项目添加对 Sales.Messages 项目的引用。
FIGURE 12.9 Adding a reference to the Sales.Messages project from the DDDesign.Web project.
网站要发送命令,需要内部运行 NServiceBus。您可以使用 Visual Studio 中的 NuGet 包管理器控制台添加对 NServiceBus 的引用来实现这一点。只需运行Install-Package命令,如图12.10和以下代码片段所示:
For the website to send commands, it needs NServiceBus running inside it. You can make that happen by adding a reference to NServiceBus using the NuGet package manager console inside Visual Studio. Just run the Install-Package command, as demonstrated in Figure 12.10 and the following snippet:
Install-Package NServiceBus –ProjectName DDDesign.Web -Version 4.3.3
Install-Package NServiceBus –ProjectName DDDesign.Web -Version 4.3.3
图 12.10使用 NuGet 包管理器控制台安装 NServiceBus。
FIGURE 12.10 Installing NServiceBus with the NuGet package manager console.
现在您已经引用了 NServiceBus 依赖项,您需要配置 NServiceBus 使其在 Web 应用程序内部实际运行。NServiceBus 使用自托管选项使此操作变得非常容易,您可以在Global.asax.cs中配置该选项,如清单 12-1 所示。
Now that you have referenced the NServiceBus dependencies, you need to configure NServiceBus to actually run inside the web application. NServiceBus makes this quite easy with the self-hosting option, which you can configure in Global.asax.cs, as shown in Listing 12-1.
如清单 12-1 所示,不需要太多配置。然而,这几行代码中有一些关键细节。首先,配置的最后一行SendOnly()告诉 NServiceBus,此应用程序仅发送消息,不接收消息。如果你回顾容器图,就会发现这完全符合设计。在 NServiceBus 的命名法中,仅发送消息的应用程序被称为客户端。
As Listing 12-1 shows, there isn’t too much configuration needed. However, among those few lines are some key details. First, the last line of configuration, SendOnly(), tells NServiceBus that this application only sends messages and does not receive them. If you look back at the containers diagram, you can see that this is exactly as per the design. In NServiceBus nomenclature, a send-only application is known as a client.
DefiningCommandsAs ()方法用于指定一个约定,让 NServiceBus 知道解决方案中的哪些类是命令。在本例中,您可以看到它是任何命名空间包含单词Commands 的类。如果您回顾一下定义PlaceOrder命令的位置,您会发现它位于名为Commands的文件夹中,因此将按照此约定进行选择。您不必强制使用此约定;您可以使用任何您喜欢的约定,甚至可以使用 XML 配置。
The DefiningCommandsAs() method is used to specify a convention that lets NServiceBus know which classes in the solution are commands. In this example, you can see it is any class whose namespace contains the word Commands. If you look back to where you defined the PlaceOrder command, you’ll see that it is inside a folder called Commands and therefore will be picked up by this convention. You don’t have to use this convention; you can use any convention you like, and you can even use XML configuration instead if you prefer.
另一个关键的配置是UseTransport<Msmq>()这行。正如本章开头所述,消息总线通常将消息存储在队列中以实现容错,以便在出现错误时可以重试消息。这行配置指示 NServiceBus 使用 Microsoft 的 MSMQ。您也可以使用 RabbitMQ 或 ActiveMQ。
The other key piece of configuration is the line UseTransport<Msmq>(). As mentioned at the start of the chapter, a message bus usually stores messages in a queue for fault tolerance so that messages can be retried if there is an error. This line of configuration instructs NServiceBus to use Microsoft’s MSMQ. You can also use RabbitMQ or ActiveMQ.
现在总线已配置完毕并可用,您可以使用它来发送命令。在本例中,我们通过添加一个OrdersController 控制器来实现,网页会将用户的订单回发到该控制器。然后, OrdersController 控制器会使用您之前配置的总线发送PlaceOrder命令。要添加OrdersController 控制器,请在DDDesign.Web项目中右键单击Controllers文件夹,然后选择“添加控制器”。输入OrdersController作为控制器名称,然后单击“添加”。图 12.11演示了此过程。
Now that the bus is configured and available, you can use it to send commands. In this example, that is accomplished by adding an OrdersController that a web page posts the user’s order back to. The OrdersController then sends the PlaceOrder command using the bus you configured earlier. To add the OrdersController, right-click on the Controllers folder inside the DDDesign.Web project and choose Add Controller. Type in OrdersController as the controller name and click Add. Figure 12.11 illustrates this.
图 12.11添加订单控制器。
FIGURE 12.11 Adding the Orders controller.
创建OrdersController后,您可以使用清单 12-2 中的代码替换文件的全部内容。
Once you’ve created the OrdersController, you can replace the entire contents of the file with the code in Listing 12-2.
此示例特意简化,以突出消息传递的方面。无论如何,使用 NServiceBus 发送消息相对简单。只需创建消息实例,并将其与收件人名称一起传递给Bus.Send(),该函数会异步发送消息。发送命令时,请确保指定收件人,因为命令仅在一个位置处理。稍后您将看到,发布事件时无需指定收件人。记住这一点很有用。
This example is intentionally quite simplistic to accentuate the messaging aspects. Sending messages with NServiceBus is relatively straightforward anyway. You just create an instance of your message and, together with the name of the recipient, pass it to Bus.Send(), which sends the message asynchronously. When sending commands, make sure you specify the recipient because commands are only handled in a single place. Later you will see that specifying the recipients is not necessary when publishing events. This is a useful distinction to remember.
您还可以在本例中看到最终一致性的体现。在订单被系统各部分成功处理之前,用户会立即收到响应。这一点在第11章中特别强调过,因为它能够在用例中的任何步骤失败时提供容错能力。如果您还记得的话,它还允许网站独立于系统的其他部分进行扩展,因为它不像远程过程调用 (RPC) 解决方案那样等待其他部分完成处理。如果您对这些优势还不甚了解,可以简要回顾一下上一章的内容。
You can also see in this case an example of eventual consistency. The user gets an immediate response before the order has successfully been processed by each part of the system. This was specifically highlighted in Chapter 11 because it provides fault tolerance should any of the steps in the use case fail. If you remember, it also allows the website to be scaled independently of other parts of the system because it does not wait for them to do processing as would be the case with remote procedure call (RPC) solutions. It would be useful to briefly revisit the last chapter if these benefits aren’t clear.
在发送PlaceOrder命令之前,只剩下一件事:需要添加一个带有表单的网页来输入订单详细信息。要创建该网页,首先需要在DDDesign.Web项目的Views文件夹中添加一个名为Orders的文件夹。在新创建的Orders文件夹中,添加一个名为Index.cshtml的文件。然后删除Index.cshtml的全部内容,并将其替换为清单 12-3 的内容。如果操作正确,解决方案结构应类似于图 12.12。
Only one thing now remains before PlaceOrder commands are sent: a web page with a form to enter the order details needs to be added. To create that web page, you first need to add a folder called Orders inside the Views folder in the DDDesign.Web project. Inside the newly created Orders folder, add a file called Index.cshtml. Then delete the entire contents of Index.cshtml and replace it with the contents of Listing 12-3. If you have done this correctly, your solution structure should resemble Figure 12.12.
图 12.12添加订单控制器和视图后的解决方案结构。
FIGURE 12.12 Solution structure after adding Orders controller and view.
清单 12-3 只是一个简单的 HTML 表单,用于下订单。图 12.5展示了该页面的渲染效果。
Listing 12-3 is just a simple HTML form that allows the order to be placed. Figure 12.5 shows how this page appears when rendered.
现在,Web 应用程序已经包含一个允许客户创建订单的页面和一个将使用提供的详细信息在总线上发送PlaceOrder命令的控制器,您需要实际处理PlaceOrder命令。回顾图 12.3中的图表,您可以看到PlaceOrder命令由销售边界上下文处理,该上下文处理该命令并发布OrderCreated事件。因此,在本节中,您将在销售边界上下文中创建一个包含此逻辑的组件。
Now that the web application has a page that allows customers to create orders and a controller that will use the supplied details to send a PlaceOrder command on the bus, you need to actually handle PlaceOrder commands. Looking back to the diagram in Figure 12.3, you can see that PlaceOrder commands are handled by the Sales bounded context, which processes the command and publishes an OrderCreated event. So in this section, you’ll create a component in the Sales bounded context that contains this logic.
NServiceBus 服务器类似于 Windows 服务——它们是在后台运行且没有 UI 的应用程序。在本例中,添加了一个 NServiceBus 服务器来处理Sales限界上下文中的PlaceOrder命令。按照{BoundedContext}.{BusinessComponent}.{Component}的命名约定,添加一个名为Sales.Orders.OrderCreated的新 C# 类库。OrderCreated 名称表示组件发布的消息类型。这是本章中使用的组件命名约定。
NServiceBus servers are like Windows Services—they are applications that run in the background without a UI. In this example, an NServiceBus server is added to handle PlaceOrder commands inside the Sales bounded context. Following the naming convention of {BoundedContext}.{BusinessComponent}.{Component}, add a new C# class library called Sales.Orders.OrderCreated. The name OrderCreated indicates the message type published by the component. This is the convention for naming components used in this chapter.
要将你的类库转换为 NServiceBus 服务器,你需要安装NServiceBus.Host NuGet 包。你可以在 Visual Studio 的 NuGet 包管理器控制台中运行以下命令(一行命令即可):
To turn your class library into an NServiceBus server, you need to install the NServiceBus.Host NuGet package. You can do that by running the following command in the NuGet package manager console inside Visual Studio (it should be entered as a single-line command):
Install-Package NServiceBus.Host -ProjectName
Sales.Orders.OrderCreated -Version 4.3.3
Install-Package NServiceBus.Host -ProjectName
Sales.Orders.OrderCreated -Version 4.3.3
NServiceBus 服务器配置了许多常见的默认设置。但它们不知道您的自定义约定。要在Sales.Orders.OrderCreated项目中设置约定,请将 NServiceBus 自动创建的EndpointConfig类的内容替换为清单 12-4 所示的代码。它所做的就是将您为 Web 应用程序设置的默认设置应用于定位命令,并添加类似的事件定位约定。请注意,此类已更新为继承的两个附加接口:IWantCustomInitialization和AsA_Publisher。
NServiceBus servers come configured with many common defaults. But one thing they don’t know about are your custom conventions. To set the conventions in the Sales.Orders.OrderCreated project, replace the contents of the EndpointConfig class that NServiceBus automatically creates with the code shown in Listing 12-4. All it’s doing is applying the same defaults for locating commands that you set up for the web application, plus a similar convention for locating events. Do note the two additional interfaces that this class has been updated to inherit: IWantCustomInitialization andAsA_Publisher.
您需要在Sales.Orders.OrderCreated组件中处理PlaceOrder命令。为此,您需要添加对Sales.Messages项目的引用。
Inside your Sales.Orders.OrderCreated component, you want to handle PlaceOrder commands. To be able to do this, you need to add a reference to the Sales.Messages project.
处理消息的一个有用约定是创建一个名为{MessageName}Handler的类。因此,在这个例子中,您应该在Sales.Orders.OrderCreated项目的根目录中创建一个名为PlaceOrderHandler的类。完成后,您可以使用清单 12-5 所示的代码替换其中的内容。
A useful convention for handling messages is to create a class called {MessageName}Handler. So in this example, you should create a class called PlaceOrderHandler in the root of the Sales.Orders.OrderCreated project. Once you’ve done that, you can replace the contents with the code shown in Listing 12-5.
清单 12-5 显示PlaceOrderHandler只是将接收到的PlaceOrder命令的详细信息打印到控制台。这只是暂时的,以便您可以运行应用程序并查看到目前为止的所有工作情况。但是,关于 NServiceBus 消息处理程序,有一些重要的细节需要注意。首先,NServiceBus 知道任何继承IHandleMessages<T> 的类都会处理类型T的消息。其次,如果您声明一个名为Bus的IBus类型的属性,它将注入一个IBus实例。这非常有用,正如您即将看到的,因为它允许您在消息处理程序中发送其他消息,包括事件。
Listing 12-5 shows the PlaceOrderHandler simply printing the details of the received PlaceOrder command to the console. This is just temporary so that you can run the application and see everything working so far. However, there are some important details to note regarding NServiceBus message handlers. First, NServiceBus knows that any class inheriting IHandleMessages<T> handles messages of type T. Second, it will inject an instance of IBus if you declare a property of type IBus named Bus. That’s quite useful, as you are about to see shortly, because it allows you to send other messages, including events, inside your message handlers.
在继续发布事件之前,这是一个很好的机会来测试到目前为止一切是否正常,并且你对消息传递的工作原理有一个基本的了解。NServiceBus 让这一切变得简单在本地计算机上调试分布式系统。您需要做的就是配置每个项目以使其正确启动,具体步骤如下:
Before moving on to publishing events, this is a good opportunity to test that everything is working so far and that you have a basic understanding of how messaging works. NServiceBus makes it easy to debug a distributed system on your local machine. All you need to do is configure each project to start up appropriately, as detailed in the following steps:
图 12.13选择解决方案属性。
FIGURE 12.13 Selecting solution properties.
图 12.14配置每个项目的启动。
FIGURE 12.14 Configuring each project to start up.
此时,您可以按 F5 开始调试应用程序。Web 应用程序将在浏览器中加载,并且销售边界上下文将在控制台中加载。如果仔细查看控制台,您会发现 NServiceBus 处理了很多繁琐的工作,例如创建所有必要的队列,如图12.15所示。
At this point, you can press F5 to start debugging the application. The web application loads in the browser, and the Sales bounded context loads in a console. If you look carefully at the console, you can see that NServiceBus takes care of lots of the hard work, such as creating all the necessary queues, as shown in Figure 12.15.
图 12.15 NServiceBus 服务器正在设置应用程序。
FIGURE 12.15 An NServiceBus server setting up an application.
要测试系统,您需要在自动打开的浏览器中导航到/orders操作并填写表单。(您只需在浏览器自动打开的 localhost URL 后面附加“orders”即可。)然后查看主机的控制台输出,您将看到它打印了收到的消息的详细信息。
To test the system, you need to navigate to the /orders action in the browser that automatically opened and fill out the form. (You just need to append “orders” to the localhost URL the browser automatically opens with.) Then look back at the console output of the host, and you will see it printing the details of the message it received.
现在,Web 应用程序正在发送销售限界上下文成功接收的命令,组件图上的第二步显示销售限界上下文需要创建订单,并在完成后发布OrderCreated事件。首先,您需要在Sales.Messages项目中定义OrderCreated事件。与创建PlaceOrder命令的方式类似,只需在项目根目录中添加一个名为Events的文件夹,然后在其中添加一个名为OrderCreated的类,如图 12.16所示。
Now that the web application is sending commands that the Sales bounded context is successfully receiving, the second step on your component diagram shows that the Sales bounded context needs to create the order and publish an OrderCreated event when it has done so. To begin, you need to define the OrderCreated event in the Sales.Messages project. Similar to the way you created the PlaceOrder command, simply add a folder called Events in the root of the project, and then add a class called OrderCreated inside it, as shown in Figure 12.16.
图 12.16将 OrderCreated 事件添加到 Sales.Messages 项目。
FIGURE 12.16 Adding the OrderCreated event to the Sales.Messages project.
在OrderCreated事件中,您需要添加事件所需的必要信息。只需将它们作为属性添加到类中即可,如清单 12-6 所示。
Inside your OrderCreated event, you need to add the necessary pieces of information that are part of the event. Just add them as properties to the class, as shown in Listing 12-6.
现在您已经定义了OrderCreated事件,您可以在需要时自由发布它。要发布任何事件,请调用Bus.Publish(),并传入该事件的实例。清单 12-7 显示了更新后的PlaceOrderHandler将订单保存到数据库,然后通过发布OrderCreated事件通知相关方已完成此操作。
Now that you’ve defined the OrderCreated event, you are free to publish it when you need to. To publish any event, invokeBus.Publish(), passing in an instance of the event. Listing 12-7 shows the updated PlaceOrderHandler saving the order to the database, and then notifying interested parties it has done so by publishing an OrderCreated event.
为了使发布事件有价值,您需要能够处理它们并应用您的域策略。
For publishing events to be worthwhile, you need to be able to handle them and apply your domain policies.
处理事件实际上与处理命令相同。为了演示,现在您将看到如何在组件图(图 12.3)上实现下一步——步骤 3——该步骤涉及计费有界上下文订阅并处理销售有界上下文发布的OrderCreated事件。您的组件图还显示步骤 4 涉及计费有界上下文发布PaymentAccepted事件。因此,按照组件以其发送的消息命名的惯例,您现在需要向解决方案中添加一个名为Billing.Payments.PaymentAccepted的新 C# 类库项目。在新项目中,您需要执行几个熟悉的步骤:
Handling events is really just the same as handling commands. To demonstrate, you’ll now see how to implement the next step on your component diagram (Figure 12.3)—step 3—which involves the Billing bounded context subscribing to and handling the OrderCreated event that the Sales bounded context publishes. Your component diagram also shows that step 4 involves the Billing bounded context publishing a PaymentAccepted event. So, following the convention of naming components after the messages they send, you now need to add a new C# class library project to the solution called Billing.Payments.PaymentAccepted. Inside your new project, you need to carry out a few familiar steps:
使用包管理器添加对 NServiceBus 包的引用(作为单行命令运行):
Install-Package NServiceBus.Host -ProjectName
Billing.Payments.PaymentAccepted -Version 4.3.3Add a reference to the NServiceBus packages using the package manager (run as a single-line command):
Install-Package NServiceBus.Host -ProjectName
Billing.Payments.PaymentAccepted -Version 4.3.3将事件定位约定添加到EndpointConfig。此组件将发布消息,因此在同一文件中,通过继承AsA_Publisher将其配置为发布者:
using NServiceBus;
namespace Billing.Payments.PaymentAccepted
{
public class EndpointConfig : IConfigureThisEndpoint,
AsA_Server, IWantCustomInitialization, AsA_Publisher
{
public void Init()
{
Configure.With()
.DefiningEventsAs(t => t.Namespace != null
&& t.Namespace.Contains("Events"));
}
}
}Add your event-locating conventions to EndpointConfig. This component will be publishing messages, so in the same file, configure it as a publisher by inheriting AsA_Publisher:
using NServiceBus;
namespace Billing.Payments.PaymentAccepted
{
public class EndpointConfig : IConfigureThisEndpoint,
AsA_Server, IWantCustomInitialization, AsA_Publisher
{
public void Init()
{
Configure.With()
.DefiningEventsAs(t => t.Namespace != null
&& t.Namespace.Contains("Events"));
}
}
}将OrderCreatedHandler添加到项目的根目录:
using NServiceBus;
using Sales.Messages.Events;
using System;
namespace Billing.Payments.PaymentAccepted
{
public class OrderCreatedHandler : IHandleMessages<OrderCreated>
{
public IBus Bus { get; set; }
public void Handle(OrderCreated message)
{
Console.WriteLine(
"Received order created event: OrderId: {0}",
message.OrderId
);
}
}
}Add an OrderCreatedHandler to the root of the project:
using NServiceBus;
using Sales.Messages.Events;
using System;
namespace Billing.Payments.PaymentAccepted
{
public class OrderCreatedHandler : IHandleMessages<OrderCreated>
{
public IBus Bus { get; set; }
public void Handle(OrderCreated message)
{
Console.WriteLine(
"Received order created event: OrderId: {0}",
message.OrderId
);
}
}
}使用 NServiceBus 处理事件时,还需要一个额外的步骤。您需要更新订阅者项目中的App.config ,以识别其订阅的事件消息的来源。在本例中, Billing.Payments.PaymentAccepted组件想要订阅由Sales.Orders.OrderCreated组件发布的OrderCreated事件。因此,Billing.Payments.PaymentAccepted中的App.config应该进行更新,如清单 12-8 所示。
There’s one additional step required when handling events with NServiceBus. You need to update the App.config in the subscriber’s project to identify the source of the event messages it subscribes to. In this case, the Billing.Payments.PaymentAccepted component wants to subscribe to OrderCreated events published by the Sales.Orders.OrderCreated component. So the App.config in Billing.Payments.PaymentAccepted should be updated, as shown in Listing 12-8.
您在第 11 章中看到,响应式解决方案的主要优势之一是提高了容错能力。您看到的一个特殊场景是外部服务(例如支付提供商)的宕机。在旧的 RPC 场景中,订单会失败。但在本解决方案中,正如您将看到的,您可以使用消息网关将自身与外部服务故障隔离开来。消息网关将不可靠的通信封装在一条消息中,以便可以重试,直到受灾服务恢复可用。
You saw in Chapter 11 that one of the major benefits of a reactive solution is improved fault tolerance. One particular scenario you saw was downtime of external services, such as the payment provider. In the old RPC scenario, the order would have failed. But in this solution, as you will see, you can insulate yourself from failures that happen to external services by using a messaging gateway. Messaging gateways wrap unreliable communication with a message so that it can be retried until the stricken service is available again.
在实现消息网关之前,这里有一个简单的例子来帮助你充分理解为什么需要它。假设OrderCreatedHandler按照清单 12-9 实现。
Before implementing the messaging gateway, here’s a quick example to help you fully understand why it’s needed. Imagine that the OrderCreatedHandler was implemented as per Listing 12-9.
你能看出清单 12-9 中哪些情况可能会惹恼付费客户吗?如果你需要线索,可以想想与外部服务的通信。如果在执行Database.SavePaymentDetails()时数据库宕机了,会发生什么?消息会失败并被放回队列。但此时你已经从客户的信用卡中扣款了。当重试发送消息时,客户会被重复扣款。这正是你需要消息网关的原因。
Can you see possible conditions related to Listing 12-9 that would annoy paying customers? If you need a clue, think about communication with external services. What would happen if the database was down when Database.SavePaymentDetails() was executed? The message would fail and be put back in the queue. But you’ve already charged the customer’s credit card at this point. When the message is retried, the customer is charged again and again. This is precisely why you need a messaging gateway.
本质上,消息传递网关将一个大事务分成两半,因此如果其中一个调用失败,您就不必重复已经执行的操作(例如向客户的信用卡收费)。在计费限界上下文,与支付提供商通信是其中的一半,更新数据库是另一半。要实现消息网关,只需在它们之间添加一条额外的消息,这是本示例的下一步。
Essentially, messaging gateways split one big transaction in half, so if one of the calls fails, you don’t repeat actions that have already been carried out (like charging a customer’s credit card). In the Billing bounded context, communicating with the payment provider is one-half and updating the database is the other. To implement a messaging gateway, you just need to add an extra message in-between them, which is the next step of this example.
如上所述,消息网关只是一种模式;它指的是将一个消息处理程序拆分成两个或多个,用于处理不可靠的网络通信。因此,消息框架并没有提供任何特殊功能——您只需创建并发送额外的消息即可。然而,真正的细节在于确保尽可能隔离有风险的通信。
As mentioned, messaging gateways are just a pattern; it’s a case of splitting one message handler into two or more, where there is unreliable network communication. Therefore, there is no special facility provided by messaging frameworks—you just need to create and send additional messages. The real detail, though, is ensuring that you isolate the risky communication as much as possible.
OrderCreatedHandler的首要职责是与外部供应商沟通。您可以触发一个事件来表明这种情况已经发生。但是,由于消息传递网关更多的是容错的实现细节,而不是领域事件,您不想将其暴露给其他组件。因此,使用命令来确保它只在一个地方处理是有意义的。在这个例子中,该命令被称为RecordPaymentAttempt。从组件图中您可以知道Billing.Payments.PaymentAccepted组件也需要发布一个PaymentAccepted事件。因此,您可以将这两条消息添加到一个名为Billing.Messages的新 C# 类库中。记住将命令放在Commands文件夹中,将事件放在Events文件夹中,如图12.17所示。这两条消息的格式如清单 12-10 所示。
The first responsibility of the OrderCreatedHandler is communicating with the external supplier. You could fire an event indicating this has happened. However, because the messaging gateway is more of an implementation detail for fault tolerance and not a domain event, you don’t want to expose it to other components. Therefore, it makes sense to go with a command to ensure it is only handled in a single place. In this example, the command is called RecordPaymentAttempt. You know from the component diagram that the Billing.Payments.PaymentAccepted component also needs to publish a PaymentAccepted event. So you can add both of those messages to a new C# class library called Billing.Messages. Remember to put the command in a Commands folder and the event in an Events folder, as Figure 12.17 illustrates. The format of those two messages is shown in Listing 12-10.
图 12.17将两条消息添加到 Billing.Messages 项目。
FIGURE 12.17 Adding the two messages to the Billing.Messages project.
现在,您可以添加健壮的OrderPlacedHandler了,它不会包含清单 12-9 中版本的漏洞。在此版本中,对支付提供商的调用被包装在RecordPaymentAttempt命令中。然后,在下一个处理程序中,将支付详细信息保存到数据库。不要忘记在Billing.Payments.PaymentAccepted组件中添加对Billing.Messages的引用。然后,您可以添加如清单 12-11 所示的OrderCreatedHandler。
You’re now ready to add the robust OrderPlacedHandler that doesn’t contain the vulnerabilities of the version in Listing 12-9. In this version, the call to the payment provider is wrapped with a RecordPaymentAttempt command. Saving the payment details to the database then comes in the next handler. Don’t forget to add a reference to Billing.Messages in the Billing.Payments.PaymentAccepted component. You can then add the OrderCreatedHandler shown in Listing 12-11.
需要注意的一个小细节是Bus.SendLocal()调用。希望这很有意义,因为发送组件也需要处理该命令。SendLocal ()只是将消息发送到发送该消息的同一个 NServiceBus 主机,然后它会在那里查找该消息的处理程序。
One small detail to note is the Bus.SendLocal() call. Hopefully this makes sense, because the sending component also needs to handle the command. SendLocal() just sends the message to the same NServiceBus host that sent the message, where it then looks for a handler of that message.
要完成此步骤,请为RecordPaymentAttempt命令添加一个处理程序,该命令模拟将付款记录到数据库。清单 12-12 展示了此处理程序的外观。如果这有助于您在脑海中拼凑消息流,则可以将其与OrderCreatedHandler放在同一个文件中。
To complete this step, add a handler for the RecordPaymentAttempt command that simulates recording the payment to the database. Listing 12-12 shows how this handler should look. You can put it in the same file as the OrderCreatedHandler if that helps you to mentally piece together the flow of messages.
现在是一个真正理解使用消息传递价值的绝佳机会。您将看到消息总线在外部服务不可用时重试消息。在此过程中,本示例充分利用了您刚刚实现的消息网关。为了模拟失败,您需要修改存根支付提供程序的实现,使其在前两次调用时抛出异常,然后顺利地接受第三次请求。
Now is a great opportunity to really understand the value of using messaging. You’re going to actually see the message bus retry messages because an external service is unavailable. In the process, this example fully makes use of the messaging gateway you just implemented. What you need to do to simulate failure is change your stub payment provider implementation to throw an exception the first two times it is called and then happily accept the third request.
如果您更新PaymentProvider以使其类似于清单 12-13,那么您几乎就可以看到您的反应式系统的容错能力了。
If you update the PaymentProvider so that it resembles Listing 12-13, you will be almost ready to see the fault tolerance of your reactive system in action.
在本章前面测试第一个消息处理程序之前,您必须将每个 NServiceBus 组件设置为在解决方案启动时启动。现在,您还需要对Billing.Payment.PaymentAccepted组件执行此操作,以便测试消息网关。因此,请确保您的启动设置类似于图 12.18。
Before you tested your first message handler earlier in this chapter, you had to set each NServiceBus component to start up when the solution was started. You also need to do that now for the Billing.Payment.PaymentAccepted component so that you can test your messaging gateway. So ensure you’re start-up settings resemble Figure 12.18.
图 12.18设置所有 NServiceBus 服务器启动。
FIGURE 12.18 Setting all your NServiceBus servers to start up.
现在您可以按 F5 运行应用程序。完成后,在浏览器中导航到/orders页面并下订单。请特别注意Billing.Payments.PaymentAccepted组件的控制台窗口。首先,您会看到OrderCreatedHandler收到了OrderCreated事件。在其下方,您会看到PaymentAccepted事件已发布,如图12.19所示。但是,如果您在控制台中向上滚动窗口,你实际上看到这是第三次尝试。图 12.20展示了 NServiceBus 在处理第一条消息失败时打印到控制台的消息。看看它甚至显示了你在存根支付提供商中抛出的异常类型和消息:System.Exception: Service unavailable. Down for maintenance(系统异常:服务不可用,停机维护)。
Now you can press F5 to run the application. After you’ve done that, navigate to the /orders page in your browser and place an order. Pay special attention to the console window for the Billing.Payments.PaymentAccepted component. At first you will just see that OrderCreatedHandler received the OrderCreated event. Below that you’ll see that the PaymentAccepted event was published, as shown in Figure 12.19. But if you scroll up in the console window, you actually see that was the third attempt. Figure 12.20 shows you the messages NServiceBus printed to the console when it failed to process the first message. Look how it even shows the exception type and message you threw in your stub payment provider: System.Exception: Service unavailable. Down for maintenance.
图 12.19看起来付款已成功处理。
FIGURE 12.19 Looks like the payment was processed successfully.
图 12.20实际上,付款首先失败了两次。
FIGURE 12.20 Actually, the payment failed twice first.
如果支付提供商长时间处于宕机状态,比如十分钟,NServiceBus 可能已经达到了重试消息的次数限制。幸运的是,NServiceBus 有一个称为2 级重试 的功能,允许您指定覆盖更长持续时间的自定义重试周期。在此示例中,每十分钟重试一次消息,持续一小时会很有用。清单 12-14 中的配置展示了如何执行此操作。
If the payment provider is down for a long time, say ten minutes, NServiceBus may have reached the limit for the number of times it retries the message. Fortunately, NServiceBus has a feature called 2nd level retries, allowing you to specify custom retry periods that cover longer durations. In this example, it would be useful to retry the message every ten minutes for an hour. The configuration in Listing 12-14 shows how to do that.
有时,消息永远不会成功,因为系统无法处理它们。可能是消息未得到正确验证,或者处理该消息所需的数据被从数据库中删除。这些被称为毒害消息,最终必然会被放入错误队列。您将在本章后面了解更多关于错误队列的信息,包括如何监控它以及如何处理最终进入错误队列的消息。
Sometimes messages will never succeed because the system cannot handle them. Perhaps a message wasn’t validated properly, or data got wiped from the database that needed to be looked up to process the message. These are known as poison messages and inevitably end up in the error queue. You’ll learn more about the error queue later in this chapter, including how to monitor it and handle messages that end up in it.
在下单流程的这个阶段,系统可以被认为处于不一致状态。尽管订单是由销售限界上下文创建的,并且付款已由结算限界上下文处理,但发货限界上下文并不知道这一点,因此尚未安排发货。在某些系统中,这种情况永远不会发生,因为整个流程是一个原子事务,要么完全成功,要么完全失败。因此,这可以被视为最终一致性的一个例子。当您使用消息传递集成限界上下文时,这是一个学习如何处理最终一致性的好时机。
At this point in the place order process, the system could be considered to be in an inconsistent state. Although the order was created by the Sales bounded context and payment has been processed by the Billing bounded context, the Shipping bounded context does not know this and hasn’t arranged shipping yet. This would never happen in some systems, where the whole series is a single atomic transaction that succeeds or fails completely. So this can be considered an example of eventual consistency. It’s an opportune moment for you to learn how to deal with eventual consistency when you are integrating bounded contexts using messaging.
最终一致性可能会导致一些不良后果。例如,如果付款被拒绝,您不能像许多非最终一致性系统那样直接回滚事务而不创建订单;该订单已作为先前事务的一部分在另一个组件中创建,并且当前位于该组件的数据库中。但是,您可以做的是前滚到新状态。您可能会告诉客户订单无法完成,因为付款失败。理想情况下,您应该在客户尝试下单时立即告知她。但是,您必须记住,您正在尝试构建一个可扩展的容错解决方案,因此需要做出牺牲。为了让其他所有人获得卓越的用户体验,让少数无法成功下单的客户感到不满通常是一种可以接受的权衡。
Eventual consistency can lead to undesirable scenarios. For example, if a payment has been rejected, you can’t just roll back the transaction and not create the order (as many non-eventually consistent systems would); the order was already created as part of a previous transaction in a different component and currently lives in that component’s database. What you can do, though, is roll forward into a new state. You’d probably tell the customer the order could not be completed because payment failed. Ideally you would tell her immediately when she tries to place an order. However, you have to remember that you’re trying to build a scalable fault-tolerant solution and you need to make sacrifices. Upsetting the few customers who cannot successfully place orders so that everybody else gets a superior user experience is often an acceptable trade-off.
当您处于不一致的状态时,您需要向前滚动到代表业务愿望或您正在建模的实际领域流程的新状态。
When you are in an inconsistent state, you need to roll forward into a new state that represents the wishes of the business or the real-world domain processes you are modeling.
为了处理订单失败的最终一致性问题,您可以与业务部门合作,创建一个新的支付失败用例的组件图,并用它来查看需要进入的状态。假设您与业务部门沟通后,被告知当付款被拒绝时,销售团队有责任通知客户订单已被取消。图 12.21展示了此用例的部分组件图。
To deal with the eventual consistency of a failed order, you can work with the business to create a new component diagram of the payment failed use case and use it to see what state you need to move into. Imagine that you did speak to the business and were told that when a payment is rejected, it is the Sales team’s responsibility to inform the customer the order was cancelled. Figure 12.21 shows a partial component diagram for this use case.
图 12.21付款被拒绝用例。
FIGURE 12.21 The payment rejected use case.
如图 12.21所示,要处理这种最终一致的场景,您只需在账单限界上下文中发布一个PaymentRejected事件,并在销售限界上下文中通过向客户发送电子邮件来处理该事件。要实现此用例,您只需发送和处理几条消息。为了避免此处未展示的重复,您可以参考前面的示例自行实现。
As you can see in Figure 12.21, all you need to do to deal with this eventually consistent scenario is publish a PaymentRejected event in the Billing bounded context and handle it in the Sales bounded context by sending an e-mail to the customer. To implement this use-case you just need to send and handle a few messages. To avoid repetition that is not shown here, you can implement it yourself if you want to using previous examples as a guide.
组件图上的下一步(步骤 4 的剩余部分,然后是步骤 5 和步骤 6)涉及安排发货的 Shipping 限界上下文。但有一个问题。再次查看PaymentAccepted事件:
The next steps on your component diagram (remainder of 4, then 5, and 6) involve the Shipping bounded context arranging shipping. But there’s a problem. Look at the PaymentAccepted event again:
public class PaymentAccepted
{
public string OrderId { get; set; }
}
public class PaymentAccepted
{
public string OrderId { get; set; }
}
如果您只有订单 ID,该如何安排发货?很遗憾,您无法做到,因此您需要仔细考虑各种选择。为了简单起见,假设每个用户只有一个地址,并且该地址位于Sales.Customers业务组件的数据库中。一种选择是使用 HTTP 通过 RPC 连接到Sales.Customers业务组件以获取所需数据。但这样一来,您就引入了时间耦合。如果Sales.Customers数据库或 RPC 端点发生故障,发货限界上下文将无法正常工作。如果其他限界上下文直接连接到Sales.Customers数据库,销售开发团队将无法根据自身的最佳需求重构或更改数据库,而又担心会破坏其他限界上下文。
How can you arrange shipping when all you have is the ID of an order? Unfortunately, you can’t, so you need to carefully consider your options. To keep things simple, imagine that each user has just one address and it lives inside the database belonging to the Sales.Customers business component. One option is to RPC across to the Sales.Customers business component using HTTP to get the data you need. But then you’ve introduced a temporal coupling. If the Sales.Customers database or RPC endpoint goes down, the Shipping bounded context cannot function. If other bounded contexts went straight to the Sales.Customers database, the Sales development team would not be able to refactor or change the database according to their own best needs without worry of breaking other bounded contexts.
另一种选择是在PaymentAccepted事件中发布客户的地址。这种方法也很危险,因为本质上,您是在通过将第一条消息的属性添加到第二条消息来聚合两条消息。在发送四条消息的用例中,第四条消息需要包含前三条消息的所有属性。这可能会导致高耦合度,并且难以调试数据来源。出于务实的考虑,您可以尝试在某些地方使用这种方法,但除非您有令人信服的业务案例,否则强烈建议不要这样做。
Another option is to publish the customer’s address on the PaymentAccepted event. This approach is also dangerous because, essentially, you are aggregating two messages by adding the properties from the first message to the second. In a use case in which four messages are sent, the fourth message needs to contain all the properties of the first three. This could cause high coupling and hard-to-debug issues trying to work out where data came from. Being pragmatic, you could try to get away with it in a few places, but it’s highly unrecommended unless you have a compelling business case.
在这种情况下,许多团队为了减少不必要的耦合,会选择将每个有界上下文所需的所有数据存储在本地。这通常是一个不错的选择,因为如今存储成本低廉。例如,在撰写本文时,您可以花大约 50 美元购买 1TB 的硬盘。这意味着销售有界上下文和发货有界上下文都应该存储客户的地址。很快您就会看到人们对此的一些担忧,但现在只需尝试使用这种方法实现发货有界上下文即可。这些问题稍后会得到解决。
An option that many teams take in this situation to reduce unwanted coupling is to store all the data each bounded context needs locally. This is often a good trade-off because storage is cheap these days. As an example, you can buy a 1TB hard drive for around $50 at the time of writing. What this means to you is that both the Sales bounded context and the Shipping bounded context should store the customer’s address. Shortly you’ll see some of the concerns people have with this, but for now just try to focus on implementing the Shipping bounded context using this approach. Those concerns will be addressed afterward.
图 12.22展示了用户注册流程。请注意,事件NewBusinessUserRegistered是如何发布的,以及两个有界上下文如何订阅此事件。“运输”有界上下文的“常客”业务组件接收客户的 ID 和地址。“营销”有界上下文的“在线营销”业务组件接收客户的 ID 地址以及业务所在的市场。这两个有界上下文都将数据存储在本地,即使相同的初始数据被存储在多个位置。
Figure 12.22 illustrates the user registration process. Notice how an event is published—NewBusinessUserRegistered—and two bounded contexts subscribe to this event. The Shipping bounded context’s Regular Customers business component takes the customer’s ID and address. The Marketing bounded context’s Online Marketing business component takes the customer’s ID address, and the market the business operates in. Both bounded contexts store the data locally, even though it’s the same initial data being stored in multiple places.
图 12.22多个有界上下文在本地存储同一段数据。
FIGURE 12.22 Multiple bounded contexts store the same piece of data locally.
要实现本章构建的电子商务系统的剩余部分,您将使用本地存储。在此之前,您需要创建Shipping.BusinessCustomers.ArrangeShipping组件,该组件订阅销售边界上下文的OrderCreated事件和计费边界上下文的PaymentAccepted事件。您可以通过执行以下步骤进入该阶段:
To implement the remaining part of the e-commerce system you’ve been building in this chapter, you’ll use local storage. Before that, you need to create the Shipping.BusinessCustomers.ArrangeShipping component that subscribes to the Sales bounded context’s OrderCreated event and the Billing bounded context’s PaymentAccepted event. You can get to that stage by carrying out the following steps:
如果您已正确完成所有操作,您的系统现在应该可以正常工作,直到安排发货为止。请尝试按 F5 运行应用程序,导航到/orders页面并填写表单。然后,您会看到三个控制台窗口出现,每个组件一个。在Shipping.BusinessCustomer.ArrangeShipping控制台中,您应该看到ArrangeShipping 组件存储了来自 Sales 边界上下文的OrderCreated事件的UserId和OrderId,然后在处理PaymentAccepted事件时使用UserId查找地址。您应该看到如图 12.23所示的控制台输出(其中包含您在表单中输入的值)。
If you’ve done everything correctly, your system should now be working up to the point of arranging shipping. Try running the application by pressing F5, navigating to the /orders page, and filling out the form. You then see three console windows appear—one for each component. Inside the Shipping.BusinessCustomer.ArrangeShipping console you should see that the ArrangeShipping component stored the UserId and OrderId from the Sales bounded context’s OrderCreated event, and it then used the UserId to look up the address when it handled the PaymentAccepted event. You should see console output that looks like Figure 12.23 (with the values you entered into the form).
图 12.23运输有界上下文在本地存储数据并使用它来安排运输。
FIGURE 12.23 Shipping bounded context storing data locally and using it to arrange shipping.
既然您已经了解了如何将数据存储到每个业务组件的本地,现在是时候讨论一下人们在面对这一重大理念变化时表达的一些担忧了。许多人提出数据重复的问题,因为他们担心这样做效率低下。另一些人则质疑,当数据副本分散在各处时,如何处理数据更新(例如客户更新其地址)。消息传递系统尤其容易引发消息乱序到达的问题。想象一下,用户注册后立即下单。如果要求“发货”限界上下文在存储用户地址之前安排发货,会发生什么情况?
Now that you’ve seen how to store data locally to each business component, it’s time to go over some of the concerns that people express when confronted with this considerable change in philosophy. A lot of people raise the issue of data being duplicated because they are concerned with how inefficient it may be. Others question how you can handle data updates, such as a customer updating his address, when there are duplicate copies scattered around. Messaging systems in particular raise the concern of messages arriving out of order. Imagine that a user signs up and then places an order immediately. What would happen if the Shipping bounded context was asked to arrange shipping before it had stored the user’s address?
请记住,本地存储数据并非理想之选。但它通常可以降低耦合度,为可扩展性、容错性和开发速度提供更好的平台。出于这些原因,在分布式系统中设置逻辑或数据的中心控制点可能会很危险。但这引发的担忧却十分严重。
Remember, storing data locally is not ideal. But it often reduces coupling, providing a better platform for scalability, fault tolerance, and rate of development. Having central points of control for logic or data in a distributed system can be dangerous for these reasons. But the concerns raised are serious.
两个业务组件可能存储产品价格。例如,账单限界上下文会存储最新价格,以便用户下单时按当前价格付款。但销售部门可能希望存储产品下单时的价格。想象一下你自己的在线购物。当你申请退款并收到收据时,你会收到当时支付的金额,而不是最新价格。这解释了为什么尽管限界上下文似乎复制了相同的数据,但从概念上讲,数据是不同的——它用于不同的目的,并且会因不同的原因而发生变化。
Two business components may store the price of a product. For example, the Billing bounded context will store the latest price so that when users place an order, they are charged the current price. But the Sales department will probably want to store the price of a product when the order was placed. Think about purchases you make online yourself. When you ask for a refund and you have a receipt, you get the money you paid at the time, not the latest price. This is an example of why, even though bounded contexts appear to duplicate the same data, conceptually the data is different—it’s used for different purposes, and it changes for different reasons.
企业经常会因为特价促销、产品过热或市场上出现新的替代品而调整产品价格。当这种情况发生时,一个有界上下文可能会先于另一个有界上下文更新价格。由此产生的问题是,用户可能在网站上看到一个价格,但将其添加到购物车时却被收取了不同的价格。这个常见问题可以通过在消息中添加有效期来轻松解决。以下代码片段显示带有AvailableFrom和AvailableTo字段的PriceUpdated事件,指示此消息中的价格有效的时间段:
Businesses often change the price of products for special promotions, lack of popularity, or newer alternatives appearing on the market. When this happens, there is the opportunity for one bounded context to update the price before another. A problem arising from this is that users may see one price on the website but be charged a different price when they add it to their basket. This common concern can easily be addressed by adding validity periods to a message. The following snippet shows a PriceUpdated event with AvailableFrom and AvailableTo fields that indicate the period of time that the price in this message is valid for:
public class PriceUpdated
{
public string ProductId { get; set; }
public double Price { get; set; }
public DateTime AvailableFrom { get; set; }
public DateTime AvailableTo { get; set; }
}
public class PriceUpdated
{
public string ProductId { get; set; }
public double Price { get; set; }
public DateTime AvailableFrom { get; set; }
public DateTime AvailableTo { get; set; }
}
发送带有有效期的消息时,最好尽快发送。您向每个有界上下文发出的通知越多,它就越有时间准备新消息的生效。通常,您可以将策略与业务规则保持一致。在这个例子中,您可以与业务部门沟通,询问价格变动的通知时间。然后,您可以与业务部门达成协议,例如,如果您至少提前 24 小时收到通知,您就可以保证所有价格都是最新的。
When sending messages with validity periods, it’s best to send them as soon as you can. The more notice you give to each bounded context, the more time it has to prepare for the new message taking effect. Often you can align your strategy with business rules. In this example, you could talk to the business and ask how much notice it gets for price changes. You could then agree with the business that you can guarantee all prices can be up to date if you get at least 24 hours’ notice, for example.
您刚才看到,从概念上讲,数据是不同的,因此“重复”一词在某些情况下语义上是不正确的。但从经济角度来看,它是否划算取决于您需要考虑的一系列因素。前面提到,1TB 硬盘的价格约为 50 美元。批量购买的容量越大,就越划算。云存储也非常便宜。
You’ve just seen that conceptually the data is different, so the word duplication is semantically incorrect in some cases. But whether it is financially cost effective is based on a number of factors you need to consider. It was mentioned previously that a 1TB hard drive costs around $50. The bigger your bulk purchase, the more cost efficient this becomes. Cloud storage is also incredibly cheap.
然而,那些你没有支付的部分却包含着隐性成本。如果软件组件和团队与相同的数据紧密耦合,你会损失多少?想想你的可扩展性需求;试想一下,如果团队彼此独立,你的系统构建速度能快多少。这些成本很难预测,但考虑到存储成本如此低廉,团队通常乐于接受数据复制的成本,以便专注于更快地交付业务价值。
It’s what you don’t pay for that contains hidden costs, though. How much would you lose by having software components and teams that were tightly coupled to the same data? Think about your scalability needs; try to imagine how much quicker you could build your system by having isolated teams. It’s difficult to predict these costs, but considering that storage is so cheap, teams are normally happy to accept the costs of duplicating data so they can focus on delivering business value faster.
如果用户注册并下单,但 Shipping 限界上下文在收到用户地址之前就收到了PaymentAccepted通知,会发生什么情况?在这种情况下,您可以简单地将消息放回队列,稍后重试,届时包含地址的消息应该已经被处理了。通常,这就是处理乱序消息的方法。
What would happen if a user signed up and placed an order, but the Shipping bounded context received the PaymentAccepted notification before it received the user’s address? In this kind of situation, you can simply put the message back on the queue and retry it again later, by which time the message containing the address will have been processed. In general, this is how to deal with out of order messages.
正如您在本章中到目前为止所见,将数据推送到消息系统只是故事的一部分。您还需要能够从消息系统中获取数据。同样,与某些传统做法相比,这可能需要不同的思维方式,尤其是在您将数据存储在每个业务组件本地时。您在图 12.4所示的容器图中看到了如何实现这一点的线索。答案是,有界上下文通常需要通过 HTTP 暴露其数据以供 Web 应用程序获取。
Pushing data into a messaging system, as you’ve seen so far in this chapter, is only part of the story. You also need to be able to get data out of the messaging system. Again, this can require a different mind-set compared to some traditional practices, especially when you are storing data locally in each business component. You saw a clue about how to achieve this in the containers diagram illustrated in Figure 12.4. The answer is that bounded contexts often need to expose their data over HTTP for web applications to fetch.
在下一章中,您将看到许多构建 HTTP API 以公开域功能和数据的良好实践和具体示例。在本节中,您将了解一些在消息传递系统中尤为重要的概念和权衡取舍。不过,下一章中的许多其他概念在某种程度上仍然适用。
In the next chapter, you will see lots of good practices for, and concrete examples of, building HTTP APIs to expose your domain functionality and data. In this section, you’ll learn about a few of the concepts and trade-offs that are especially important in messaging systems. Many of the other concepts from the next chapter will still be relevant to some extent, though.
如果遵循每个业务组件都有自己的私有数据库的原则,那么与 Web 应用程序共享数据的唯一合理方法是每个业务组件都有自己的一组 API 来公开这些数据。图 12.24可视化了这种建议的布局。
If you work to the principle that each business component has its own private database, the only sensible way to share data with a web application is for each business component to have its own set of APIs exposing that data. Figure 12.24 visualizes this suggested layout.
图 12.24业务组件有自己的 API。
FIGURE 12.24 Business components have their own APIs.
如图 12.24所示,一些业务组件选择使用两个 Web 应用程序来公开其数据。相比之下,其他业务组件不提供 API,因为它们不公开数据;它们只是使用数据并执行其他任务。在业务组件内部,您可以自由地进行操作。项目数量、API 数量,甚至所使用的技术都取决于您的最佳判断和项目需求。
As you can see in Figure 12.24, some business components have chosen to have two web applications exposing their data. In contrast, other business components do not provide APIs because they do not expose data; they simply consume it and carry out other tasks. Inside your business components, you are free to do as you please. The number of projects, the number of APIs, and even the technologies used are down to your best judgment and the needs of the project.
在服务器端(控制器内部)查询所有 API,然后将组合模型发送到网页,这种做法可能很诱人。然而,这却会抵消构建松耦合、容错系统所付出的大量努力,因为你又引入了一个耦合点。首先,如果其中一个 API 发生故障,可能会出现错误,导致页面无法渲染。不过,仔细想想,这真的有必要吗?想象一下,你构建了一个显示产品目录的页面,并在页面底部展示了特价商品。图 12.25通过展示页面的不同部分如何从不同的 API 获取数据,对此进行了示例说明。
It can be tempting to query all your APIs on the server (inside a controller) and send a combined model to the web page. Unfortunately, this can undo a lot of the hard work of building a loosely coupled, fault-tolerant system, because you have introduced another coupling point. First, if one of the APIs is down, there may be an error and the page may not render. Think carefully, though. Is that really necessary? Imagine that you built a page showing a catalogue of products, at the bottom of which page you showed special offers. Figure 12.25 exemplifies this by indicating how different parts of the page get their data from different APIs.
图 12.25页面从多个 API 获取数据。
FIGURE 12.25 Pages get their data from multiple APIs.
如果“营销”限界上下文出现问题,无法提供特价优惠,您认为企业会希望客户看到一个错误屏幕,阻止他们消费吗?有些企业肯定希望即使无法显示特价优惠,也能继续显示主产品列表。您可以通过服务器端编排来实现这一点,但这需要更多精力,而且很容易被遗忘。此外,Web 应用程序中执行编排的控制器是一个额外的组件,可能会失败。出于这些原因,有些团队更喜欢使用 AJAX 直接从页面内部加载数据。
Do you think the business would want customers to see an error screen preventing them from spending money if the Marketing bounded context was experiencing issues and could not provide the special offers? Some businesses would definitely prefer you to still show the main list of products, even if you couldn’t show the special offers. You could do this with server-side orchestration, but it requires more effort and can easily be forgotten. Also, the controller in your web application that performs the orchestration is an extra component that could fail. Some teams prefer to load the data directly from within the page using AJAX for these reasons.
回顾图 12.25,您可以看到页面的每个部分都从不同的 API 中提取所需的相关数据。那么,为什么不直接在页面中发出 AJAX Web 请求,从而省去服务器端编排的麻烦呢?您的 API 可以返回轻量级 JSON,而您的网页可以使用您喜欢的 JavaScript 库来管理和呈现数据。当您尝试构建快速的单页应用程序 (SPA) 或许多其他网站使用的通用 API 时,这种 UI 组合风格尤其有效。
Looking back at Figure 12.25, you can see that each part of the page pulls in the relevant bits of data it needs from the different APIs. So why not just make AJAX web requests directly in the page and remove the need for server-side orchestration? Your APIs can return lightweight JSON, while your web pages use your favorite JavaScript libraries for managing and presenting the data. This style of UI composition can work especially well when you are trying to build fast, singe-page applications (SPAs) or generic APIs that are used by many other websites.
您可以直接返回在页面上渲染的 HTML,而不是从 API 返回 JSON 或 XML 等数据。这种 UI 组合风格的支持者倾向于让每个有界上下文自行决定页面特定部分的渲染方式。例如,回顾图 12.25,如果“营销”有界上下文返回 HTML,它可以控制页面特价部分的显示方式。它甚至可以随时通过返回更新后的 HTML 来重新设置页面特价部分的样式。
Instead of returning data, such as JSON or XML, from your APIs, you can just return HTML that is directly rendered on the page. Proponents of this style of UI composition tend to prefer the ability of each bounded context to decide how certain parts of the page are rendered. Looking back at Figure 12.25, for example, if the Marketing bounded context returned HTML, it could control how the special offers section of the page appeared. It could even restyle the special offers section of the page whenever it wanted just by returning updated HTML.
虽然使用 HTML 进行 UI 组合可以为每个有界上下文提供额外的控制,但它可能难以管理页面的一致性,而当 HTML 位于单个文件中时,这通常很容易实现。当多个 Web 应用程序共享 API 时,这种方法也很难应用。但这无疑是一些团队会选择的一种方案,您至少应该考虑将其应用于项目的优势。
Although UI composition with HTML gives extra control to each bounded context, it can be hard to manage the consistency of the page that would normally be easy to achieve when the HTML lived in a single file. This approach is also difficult to apply when multiple web applications share the API. But it’s certainly an option that some teams use, and you should at least consider the advantages of applying it on your projects.
API 并不总是供内部使用。有时,它们会被多个不属于你的应用程序使用。对于某些公司来说,API 甚至是主要产品。下一章将学习如何使用 REST 等模式创建 HTTP API,并将其暴露给外部用户。
APIs aren’t always for internal use. Sometimes they are used by multiple applications that you don’t own. For some companies, an API is even the main product. You will learn in the next chapter how to create HTTP APIs, using patterns like REST, to expose them to external consumers.
构建消息传递系统后,您仍然需要应对一些重大挑战。您需要监控系统,确保其运行良好并支持业务需求。您需要了解应用程序的运行情况,并且肯定希望发现任何正在发生的错误,以便及时修复并避免收入损失。重要的是,您需要制定策略来应对影响多个团队的开发变化,例如领域事件结构的变化。
Once you’ve built a messaging system, there are still big challenges you need to deal with. You’ll need to monitor the system to ensure it is behaving well and supporting the needs of the business. You’ll want to see how well the application is performing, and you’ll definitely want to find any errors that are occurring so you can fix them and prevent lost revenue. Importantly, you will need to put in place a strategy for dealing with development changes that affect multiple teams, such as change in the structure of a domain event.
在某些时候,您几乎肯定会需要更改消息的格式。但消息格式是两个团队之间的契约。任何格式的更改都可能需要两个团队的输入和协作。虽然这些情况不可避免,但松耦合的面向服务架构 (SOA) 系统的目标之一是避免团队之间互相拖累。更改多个有界上下文之间的契约就可能发生这种情况。
At some point, you will almost certainly be required to change the format of a message. But the message format is the contract between two teams. Any changes to the format may require input and collaboration from both teams. Although these scenarios are inevitable, one of the goals of loosely coupled Service Oriented Architecture (SOA) systems is that teams don’t slow each other down. Changing the contract between multiple bounded contexts is a situation in which this could happen.
如果您只是更改消息格式并进行部署,那么尚未升级到新格式的系统将无法正常工作。您需要一种策略,让您能够以不影响现有消费者的方式更新消息格式。这样,所有相关团队都可以在需要时更新到新格式。在此之前,他们只能使用旧格式。这就是为什么在更改消息格式时需要追求向后兼容性。
If you were to just change the format of a message and deploy it, the systems that had not upgraded to the new format wouldn’t work anymore. What you need is a strategy that allows you to update message formats in a way that doesn’t break existing consumers. This would then allow all the teams involved to update to the new format when it was important for them to do so. Until then, they could use the old format. This is why you need to aim for backward compatibility when you change message formats.
保持消息的向后兼容性是确保有界上下文之间集成完整无缺的重要一步。此外,它还能确保每个有界上下文能够独立开发,避免团队在消息格式发生变化时召开大量会议并安排同步部署。不妨考虑一下本章前面提到的OrderCreated事件:
Maintaining backward compatibility with your messages can be an important step in ensuring that integration between bounded contexts remains intact. It can also be important in enabling each bounded context to be developed independently, without teams having to have lots of meetings and schedule a simultaneous rollout when the message format changes. Consider the OrderCreated event from earlier in the chapter:
public class OrderCreated
{
public string OrderId { get; set; }
public string UserId { get; set; }
public string[] ProductIds { get; set; }
public string ShippingTypeId { get; set; }
public DateTime TimeStamp { get; set; }
public double Amount { get; set; }
}
public class OrderCreated
{
public string OrderId { get; set; }
public string UserId { get; set; }
public string[] ProductIds { get; set; }
public string ShippingTypeId { get; set; }
public DateTime TimeStamp { get; set; }
public double Amount { get; set; }
}
假设企业提交了一项新的授权,允许客户拥有多个地址。该企业的所有在线竞争对手都允许这样做,因此企业确实需要建立功能对等机制。为了启用新功能,OrderCreated事件需要包含AddressId。对于许多消息传递框架来说,这是一个重大变更,这意味着在合约中添加这个新字段将导致未更新到最新版本的现有订阅者无法使用。
Imagine that a new authorization came in from the business permitting customers to have more than a single address. All the business’s online competitors allow this, so the business really needs to establish feature parity. To enable the new functionality, the OrderCreated event needs to contain an AddressId. For many messaging frameworks, this is a breaking change, meaning that adding this new field to the contract will break existing subscribers who do not update to the latest version.
不过,让我们更仔细地看一下这种情况。“发货”和“账单”限界上下文都订阅了该事件。“发货”限界上下文知道新地址很重要,这样订单才能发送到那里。但“账单”限界上下文需要知道这一点吗?在某些领域,它可能并不关心,因为它只将账单发送到客户的电子邮件地址。即使如果确实要发送纸质邮件,它会发送到账单地址,而不是收货地址。那么,是否有必要仅仅为了适应一种他们不关心的新消息格式,就让账单团队无法投入到重要的业务价值中呢?很多情况下并不需要,而且向后兼容的消息版本控制可以让那些不关心新格式的团队避免在那些不会增加业务价值的技术问题上浪费宝贵的时间。
Look at the situation a little more closely, though. Both the Shipping and Billing bounded contexts subscribe to the event. It’s important that the Shipping bounded context knows the new address so the order can be sent there. But does the Billing bounded context need to know that? In some domains, it probably doesn’t care, because it only sends bills to the customer’s e-mail address. Even if it did send paper mail, it would send it to the billing address, not the shipping address. So is there any need to divert the Billing team away from adding important business value, just to accommodate a new message format that they don’t care about? In many cases there’s not, and backward-compatible message versioning saves teams that don’t care about the new format from wasting valuable time on technical issues that don’t add business value.
消息传递框架在定义和更新消息格式的方式上可能有所不同。现在您将了解如何使用 NServiceBus 实现向后兼容的消息版本控制,但这些概念大致适用于您使用的任何消息传递框架。
Messaging frameworks can vary in how they let you define and update message formats. You’ll now see how to achieve backward-compatible message versioning with NServiceBus, but the concepts will be loosely applicable to any messaging framework you use.
在本部分中,你将使OrderCreated事件向后兼容,以便“发货”边界上下文使用新版本,而“计费”边界上下文使用现有版本。这仅涉及几个简短的步骤:
In this section, you’ll make the OrderCreated event backward compatible so that the Shipping bounded context uses the new version, whereas the Billing bounded context uses the existing version. This involves just a few short steps:
对于不关心新版本的服务,无需进行任何更改(它们仍将以旧格式接收消息)。
No changes are necessary to a service that doesn’t care about the new version (they will still get messages in the old format).
在此示例中,执行这些步骤包括创建从OrderCreated事件继承的OrderCreated_V2事件、更新Shipping.BusinessCustomers.ArrangeShipping以处理新事件,最后从 Sales 边界上下文发送OrderCreated_V2事件。
In this example, carrying out these steps involves creating an OrderCreated_V2 event that inherits from the OrderCreated event, updating Shipping.BusinessCustomers.ArrangeShipping to handle the new event, and finally sending an OrderCreated_V2 event from the Sales bounded context.
首先,在与原始OrderCreated事件相同的文件夹中添加OrderCratedEvent_V2事件。它应该如清单 12-19 所示。其次,更新后的OrderCreatedHandler应该如清单 12-20 所示,利用新的消息格式,并使用消息中提供的AddressID。还需要更新Shipping.BusinessCustomers.ShippingArranged中的App.config,以标识新的OrderCreated_V2事件的发布位置,如清单 12-21 所示。最后,您可以发送OrderCreated_V2事件,如清单 12-22 所示。
First, add the OrderCratedEvent_V2 event in the same folder as the original OrderCreated event. It should look like Listing 12-19. Second, your updated OrderCreatedHandler should look something like Listing 12-20, taking advantage of the new message format, and using the AddressID supplied in the message. You’ll also need to update App.config in Shipping.BusinessCustomers.ShippingArranged to identify where the new OrderCreated_V2 event will be published from, as per Listing 12-21. Finally, you can send an OrderCreated_V2 event, as shown in Listing 12-22.
如果您现在运行该应用程序,计费 (Billing) 限界上下文应该会像以前一样工作,处理原始消息格式(因为OrderCreated_V2仍然是OrderCreated )。运输 (Shipping) 限界上下文使用相同的OrderCreated事件,但转换为OrderCreated_V2 ,从而整合了新的地址逻辑。需要注意的是,订单使用的地址不会随OrderCreated事件发送。它将通过另一个事件发送,例如UserAddedAddress。Shipping.BusinessCustomers业务组件需要订阅此事件并在本地存储地址。
If you run the application now, the Billing bounded context should work as it used to, handling the original message format (because an OrderCreated_V2 is still an OrderCreated). The Shipping bounded context incorporates the new address logic using the same OrderCreated event but cast as an OrderCreated_V2. One thing to notice is that the address used for the order will not be sent with the OrderCreated event. It will be sent from another event, such as UserAddedAddress. The Shipping.BusinessCustomers business component needs to subscribe to this event and store the address locally.
当系统启动并运行时,密切关注任何错误非常重要,以确保用户获得可接受的服务水平。衡量 SLA 和业务指标也很重要,这样企业就可以将其作为制定关键决策的依据,例如如何改进产品或业务模式。衡量 SLA 还可以告知开发团队系统何时运行缓慢,何时需要考虑扩展。现在您将了解如何使用 NServiceBus 处理错误、衡量技术和业务指标以及扩展有界上下文。这些概念也适用于其他消息传递框架,尽管它们的实现方式可能有所不同。
When a system is up and running, it’s important to keep any eye on errors to ensure that users are getting an acceptable level of service. It’s also important to measure SLAs and business metrics so that the business can use them as the basis for making key decisions, such as how to evolve the product or the business model. Measuring SLAs also informs the development team when the system is too slow and it’s time to consider scaling. You’ll now see how to handle errors, measure technical and business metrics, and scale bounded contexts using NServiceBus. These concepts should apply to other messaging frameworks, too, although they will be implemented differently, of course.
您在本章前面看到,未成功传递或处理的消息会被重试。但在该示例中,您只是添加了一些逻辑来确保支付提供商仅失败两次。这被称为暂时性错误,因为这种情况会在一段时间后自行解决。然而,有时错误并非暂时性的,无论消息重试多少次,它总是会失败。这些是有害消息(http://msdn.microsoft.com/en-us/library/ms166137.aspx 上有更详细的讨论)。在某个时候,您的消息传递框架将停止重试有害消息。然后,大多数消息传递框架会将它们放入一个名为错误队列的特殊队列中,需要手动干预才能删除这些消息。
You saw earlier in this chapter that messages that are not successfully delivered or processed are retried. In that example, though, you just added some logic to ensure the payment provider only failed twice. This is known as a transient error, because the situation resolves itself after some time. However, sometimes errors are not transient, and no matter how many times a message is retried, it always fails. These are poison messages (discussed in more detail at http://msdn.microsoft.com/en-us/library/ms166137.aspx). At some point, your messaging framework will stop retrying poison messages. Most messaging frameworks then put them in a special queue called the error queue, where manual intervention is required to remove the messages.
您可以通过更新某个消息处理程序使其始终抛出异常来查看错误队列的实际运行情况。尝试替换Sales.Orders.OrderCreated.PlaceOrderHandler的Handle()方法中的逻辑,使其模拟永久错误情况,如下所示:
You can see the error queue in action by updating one of your message handlers to always throw an exception. Try replacing the logic in your Sales.Orders.OrderCreated.PlaceOrderHandler’s Handle() method so that it simulates a permanent error condition as follows:
public void Handle(PlaceOrder message)
{
throw new Exception("I have received a poison message");
}
public void Handle(PlaceOrder message)
{
throw new Exception("I have received a poison message");
}
现在运行应用程序并下订单会导致消息反复失败,并最终进入发送有害消息的组件的错误队列。(您需要等待控制台中出现红色错误消息。)为了确认这一点,您需要在 Visual Studio 中激活服务器资源管理器,展开“服务器”,展开您的计算机名称,然后展开“消息队列”。最后,您可以展开“私有队列”节点并检查错误队列的内容,如图12.26所示。
Running your application and placing orders now causes the message to repeatedly fail and ultimately end up in the error queue of the component that sends the poison message. (You need to wait for a red error message to appear in the console.) To confirm this, you need to activate the Server Explorer in Visual Studio, expand Servers, expand the name of your machine, and then expand Message Queues. Finally, you can expand the Private Queues node and examine the contents of the error queue, as shown in Figure 12.26.
图 12.26在 Visual Studio 的服务器资源管理器中定位错误队列。
FIGURE 12.26 Locating the error queue in Visual Studio’s Server Explorer.
要检查错误队列中的所有消息,请展开“队列消息”节点。如果要检查单条消息的内容,请右键单击该消息,然后选择“属性”。突出显示BodyStream行,然后点击右侧列中出现的三个点的按钮。图 12.27展示了查看错误队列中消息的BodyStream值的示例。
To inspect all the messages in the error queue, expand the Queue messages node. If you want to inspect the contents of a single message, right-click on it and choose Properties, highlight the BodyStream row, and click the button with three dots that appears in the right column. Figure 12.27 shows an example of viewing the BodyStream value of a message in the error queue.
图 12.27查看错误队列中的消息内容。
FIGURE 12.27 Viewing the contents of a message in the error queue.
监控错误队列是一项重要的活动,可以帮助您快速发现生产环境中的错误。但如何实际监控队列则取决于您自己。幸运的是,您有很多选择。例如,您可以使用开源 Wolfpack 监控工具 ( http://wolfpack.codeplex.com/ ),甚至可以创建自己的监控系统。
Monitoring the error queue is an important activity that lets you spot production bugs quickly. But how you actually monitor the queue is up to you. Fortunately, you have a number of choices. For example, you can use the Open Source Wolfpack monitoring tool (http://wolfpack.codeplex.com/) or even create your own monitoring system.
一旦消息被放入错误队列,就需要人工干预来确定接下来的处理方式。检查消息后,您可能会发现问题已经解决,消息应该没有问题(这实际上是一个瞬时错误),或者代码中存在错误,这意味着该消息永远无法成功处理。无论哪种情况,当问题解决后,NServiceBus 都会附带一个工具,将所有消息发送回其队列,以便再次尝试处理(ReturnToSourceQueue.exe)。
Once a message is put in an error queue, manual intervention is required to determine what happens to it next. After inspecting the message, you might realize that the problem has gone away and the message should be fine (it was really a transient error), or there is an error in the code, meaning the message will never successfully be handled. Either way, when the problem has been fixed, NServiceBus ships with a tool that sends all messages back to their queue so they can be attempted again (ReturnToSourceQueue.exe).
您可以从 NServiceBus 文档(http://support.nservicebus.com/customer/portal/articles/860511-getting-started—fault-tolerance)了解有关监控和处理错误的更多信息。
You can learn more about monitoring and dealing with errors from the NServiceBus documentation (http://support.nservicebus.com/customer/portal/articles/860511-getting-started—fault-tolerance).
过去两小时内下了多少单?处理一个订单需要多长时间?外部支付提供商收取信用卡费用需要多长时间?这些问题都可以通过测量流经系统的消息速率和频率来回答。它们对于帮助企业做出决策非常重要,对于帮助技术团队消除瓶颈并改进容量规划也至关重要。
How many orders were placed in the past two hours? How long does it take an order to be processed? How long is the external payment provider taking to charge credit cards? These are all questions that can be answered by measuring the rate and frequency of messages flowing through your system. They’re important for helping the business make decisions, and they’re important for helping the tech team remove bottlenecks and improve capacity planning.
应该能够捕获指标,并使用您偏好的技术将其输入到您自己的分析系统中。使用 MSMQ 和 NServiceBus,您可以获得很多监控优势。开箱即用。这两种技术都提供了额外的性能计数器,您可以在 NServiceBus 文档 ( http://support.nservicebus.com/customer/portal/articles/859446-monitoring-nservicebus-endpoints )中查看。然后,您可以使用 Windows Management Instrumentation ( http://msdn.microsoft.com/en-us/library/aa310909(v=vs.71).aspx ) 将这些指标导入到您首选的监控系统中。
It should be possible to capture metrics and feed them into your own analytics systems with your preferred choice of technologies. With MSMQ and NServiceBus, you get a lot of monitoring goodies out of the box. Both technologies provide additional performance counters, which you can see in the NServiceBus documentation (http://support.nservicebus.com/customer/portal/articles/859446-monitoring-nservicebus-endpoints). You can then import these metrics into your preferred monitoring system using Windows Management Instrumentation (http://msdn.microsoft.com/en-us/library/aa310909(v=vs.71).aspx).
当您的指标显示消息在队列中等待时间过长,而企业又反映客户抱怨消息处理速度过慢时,您需要扩展系统以加快消息处理速度。由于不断购买更大的服务器效率低下,您通常需要添加更多服务器并在它们之间分摊负载。在消息传递系统中,这意味着同一组件的多个实例会使用同一个队列。不幸的是,这会带来一个小问题,因为 NServiceBus 会在处理消息的机器上创建队列,并且不允许其他机器访问这些队列。不过,NServiceBus 针对这些场景提供了一个类似负载均衡器的解决方案,称为分发器。您可以快速阅读它的官方文档 ( http://support.nservicebus.com/customer/portal/articles/859556-load-balancing-with-the-distributor )来快速上手。如果您选择其他消息传递框架,它可能也会有类似的扩展解决方案。
When your metrics are telling you that messages are spending a long time in the queue, and the businesses are telling you that customers are complaining about slow turnaround times, you need to scale the system to process messages faster. Because it’s inefficient to keep buying bigger servers, you often need to add more servers and spread the load among them. In a messaging system, this means multiple instances of the same component feeding off the same queue. Unfortunately, this represents a slight problem, because NServiceBus creates queues on the machines that are processing the messages and won’t allow the queues to be accessed from other machines. However, NServiceBus has a load balancer-like solution for these scenarios called the distributor. You should be able to get going by quickly reading the official documentation for it (http://support.nservicebus.com/customer/portal/articles/859556-load-balancing-with-the-distributor). Should you choose another messaging framework, it will probably have similar solutions for scaling.
在投入更多硬件资源之前,NServiceBus 确实有一个设置可以提高它在单机上运行速度。TransportConfig节点上的MaximumConcurrencyLevel设置(如以下代码片段所示)控制着 NServiceBus 使用的线程数。以下代码片段显示了分配给 NServiceBus 的四个线程:
Before throwing more hardware at the problem, NServiceBus does have a setting that may allow it to run faster on a single machine. The MaximumConcurrencyLevel setting on the TransportConfig node, as shown in the following snippet, controls the number of threads NServiceBus will use. The following snippet shows four threads being granted to NServiceBus:
<TransportConfig MaxRetries="5" MaximumConcurrencyLevel="4" />
<TransportConfig MaxRetries="5" MaximumConcurrencyLevel="4" />
您选择消息传递是因为它能为您的架构带来极大的自由度和松散的耦合。但随后您发现自己被 NServiceBus 束缚,这实际上是另一种形式的耦合(平台耦合)。如果您想使用不同的技术构建新系统,或者整合不在 .NET 上运行的现有系统,这可能会成为一个问题。消息传递系统中的平台耦合是一个众所周知的问题,但有一个著名的解决方案:一种名为消息桥的消息传递模式。
You’ve chosen messaging because it gives you lots of freedom and loose coupling in your architecture. But then you find out you’re tied to NServiceBus, which is actually another form of coupling (platform coupling). This can be a problem if you want to build new systems in different technologies or incorporate existing systems that don’t run on .NET. Platform coupling in messaging systems is a well-known problem with a well-known solution: a messaging pattern called the messaging bridge.
在本节中,您将看到如何使用消息桥来集成另一个使用不同消息框架的服务:Mass Transit(您也可以使用消息桥与非 .NET 系统集成)。您将了解 Mass Transit 与 NServiceBus 的不同之处,并且您将学习足够的基础知识来开始使用它。但首先,试着想象一下这个场景:您在本章前面为其构建电子商务系统的公司收购了一家成功的促销初创公司。这家初创公司的平台已经设置为处理消息并在随机客户下订单时向他们发送免费礼物。要将其集成到您的电子商务 NServiceBus 系统中,被收购的平台需要能够订阅由 Sales 有界上下文放置的OrderCreated事件,如图 12.28所示。
In this section, you’ll see how you can use a messaging bridge to integrate another service that employs a different messaging framework: Mass Transit (you could also use a messaging bridge to integrate with non-.NET systems). You will learn a little about how Mass Transit differs from NServiceBus, and you’ll learn enough of the basics to get started with it. But first, try to picture this scenario: The company that you built the e-commerce system for earlier in this chapter has acquired a successful start-up that deals with promotions. The start-up’s platform is already set up to process messages and send out free gifts to random customers when they place an order. To integrate it into your e-commerce NServiceBus system, the acquired platform needs to be able to subscribe to the OrderCreated events placed by the Sales bounded context, as shown in Figure 12.28.
图 12.28促销有界上下文集成到电子商务消息传递系统中。
FIGURE 12.28 Promotions bounded context integrating into e-commerce messaging system.
不幸的是,新的 Promotions 限界上下文订阅OrderCreated事件并不容易,因为它没有使用 NServiceBus。这时,消息桥就派上用场了。
Unfortunately, it’s not so simple for the new Promotions bounded context to subscribe to the OrderCreated event because it isn’t using NServiceBus. This is where the messaging bridge comes into play.
当两个独立的消息系统需要共享消息时,消息桥(http://www.eaipatterns.com/MessagingBridge.html)是解决这个问题的一种方法。通常,使用两个独立的框架无法形成单一、完全连接的消息总线,但通过使用消息桥,您可以连接每个消息系统的端点,从而有效地创建链路,如图12.29所示。
When you have two separate messaging systems that need to share messages, a messaging bridge (http://www.eaipatterns.com/MessagingBridge.html) is one way of addressing the issue. It’s usually not possible to form a single, fully connected message bus using two independent frameworks, but by using a messaging bridge, you can connect individual endpoints from each messaging system to effectively create a link, as illustrated in Figure 12.29.
图 12.29消息传递桥就像一个链接。
FIGURE 12.29 A messaging bridge is like a link.
实现消息桥通常只需创建一个应用程序,该应用程序可以从一个消息系统接收消息,并将其传递给另一个消息系统。在您的电子商务系统中,这需要为 OrderCreated_V2 事件创建一个新的订阅者,该订阅者会转换该事件,然后将其推送到使用 Mass Transit 的新限界上下文中。
Implementing a messaging bridge usually just involves creating an application that can receive messages from one messaging system and hand them over to the other. In your e-commerce system, it’s a case of creating a new subscriber to the OrderCreated_V2 event that converts the event and then pushes it into the new bounded context that uses Mass Transit.
在构建它之前,你需要先构建一个使用公共交通的促销有界上下文。在此之前,你需要先简单介绍一下公共交通。
Before you can build that, you need to build the Promotions bounded context that uses Mass Transit. And before that, you need a brief introduction to Mass Transit.
多样性通常是一件好事,这就是为什么 .NET 开发者很幸运能够拥有 Mass Transit ( http://masstransit-project.com/ )——一个可以替代 NServiceBus 的消息传递框架,可以独立使用,也可以与其他消息传递框架配合使用。本例演示了后者。尽管 Mass Transit 的实现方式不同,并且有自己的 API,但它仍然与 NServiceBus 有很多共同之处。
Variety is often a good thing, which is why .NET developers are lucky to have Mass Transit (http://masstransit-project.com/)—an alternative messaging framework to NServiceBus that can be used stand-alone or alongside other messaging frameworks. The latter case is demonstrated in this example. Although it is implemented differently and has its own API, Mass Transit still shares many commonalities with NServiceBus.
在为应用程序的 NServiceBus 部分创建的现有解决方案中,您需要添加一个名为Promotions.LuckyWinner.LuckyWinnerSelected的新 C# 类库。然后,您可以通过在包管理器控制台中运行以下命令(每个命令都以单行命令的形式运行)将 Mass Transit 添加到其中:
Inside the existing solution you created for the NServiceBus parts of the application, you need to add a new C# class library called Promotions.LuckyWinner.LuckyWinnerSelected. Then you can add Mass Transit to it by running the following commands in the package manager console (run each as a single-line command):
Install-Package MassTransit
–ProjectName Promotions.LuckyWinner.LuckyWinnerSelected
- Version 2.9.5
Install-Package MassTransit
–ProjectName Promotions.LuckyWinner.LuckyWinnerSelected
- Version 2.9.5
和
and
Install-Package MassTransit.msmq
–ProjectName Promotions.LuckyWinner.LuckyWinnerSelected
- Version 2.9.5
Install-Package MassTransit.msmq
–ProjectName Promotions.LuckyWinner.LuckyWinnerSelected
- Version 2.9.5
此时,您的项目中已经引用了所有 Mass Transit 依赖项,因此下一步是配置 Mass Transit 总线的实例。您会很高兴地知道,配置 Mass Transit 可以纯粹用代码完成。而且,它非常精简,如清单 12-23 所示。您可以将清单 12-23 的内容添加到您的新项目中。
At this point, you have all the Mass Transit dependencies referenced in your project, so the next step is to configure an instance of the Mass Transit bus. You’ll be happy to know that configuring Mass Transit can be done purely in code. It’s quite minimal, too, as Listing 12-23 demonstrates. You can add the contents of Listing 12-23 to your new project.
在顶部,您可以看到 Mass Transit 类是通过第一个using语句导入的。在 main 方法内部,使用Bus.Initialize()配置总线,该方法会提供一个配置对象,以便您根据需要设置总线。在此示例中,总线被配置为使用 MSMQ,您应该在本章前面已经熟悉了 MSMQ。
At the top, you can see that the Mass Transit classes are imported with the first using statement. Inside the main method is where the bus is configured using Bus.Initialize(), which gives you a configuration object so you can set up the bus according to your needs. In this example, the bus is being configured to use MSMQ, which you should be familiar with from earlier in this chapter.
尽管清单 12-25 显示了使用 MSMQ 运行公交车所需的基本配置,但它并没有告诉 Mass Transit 需要处理的消息类型或如何处理它们。
Although Listing 12-25 shows the bare-bones configuration needed to get the bus running with MSMQ, it doesn’t tell Mass Transit about the kinds of messages that need to be handled or how to handle them.
为了保持极简主义的主题,Mass Transit 消息可以是任何 C# 类;它们无需以任何方式命名或从任何基类继承。由于此示例旨在使用 NServiceBus 创建消息桥,因此您可以直接引用并使用 NServiceBus 将发布的相同OrderCreated类。但是,为了使此示例独立运行,最好在此项目中创建该类的副本。这样,桥的两端就不会共享依赖关系。如果您要创建跨平台消息桥,则无论如何都不可能存在代码依赖关系。
In keeping with the minimalist theme, Mass Transit messages can be any C# class; they do not have to be named in any way or inherit from any base classes. Because this example is to create a message bridge with NServiceBus, you could just reference and use the same OrderCreated class that NServiceBus will publish. However, to make this example stand-alone, it’s best if you create a copy of the class in this project. In this way, the two sides of the bridge do not share a dependency. If you were creating a cross-platform messaging bridge, code dependencies would not be possible anyway.
在清单 12-23 创建的Program类下方,添加一个OrderCreated类来表示订单创建的领域事件。该类现在可供 Mass Transit 使用;您只需要告诉 Mass Transit 如何以及何时使用它。清单 12-24 展示了您的OrderCreated事件应该是什么样子。
Below the Program class you created from Listing 12-23, add an OrderCreated class to represent the order-created domain event. This class is now available for Mass Transit to use; you just need to tell Mass Transit how and when to use it. Listing 12-24 shows what your OrderCreated event should look like.
再次强调,在配置消息处理器时,Mass Transit 采用了轻量级的纯代码方法。要创建消息处理器,您只需在某个地方定义一个方法来处理您感兴趣的消息。然后,您可以告诉 Mass Transit 在收到所需类型的消息时调用该方法。通过一个示例,您将会明白这一点。
Again, when configuring message handlers, Mass Transit maintains a lightweight, code-only approach. To create a message handler, you just need to define a method somewhere that handles the message you are interested in. You can then tell Mass Transit to call that method when it receives a message of the required type. This will make sense after an example.
创建一个名为OrderCreatedHandler的类,它只有一个Handle()方法,该方法接受您刚刚在项目中创建的OrderCreated事件的实例。为了简单起见,您可以将此类放在同一文件中Program类的正下方,就像您对OrderCreated类所做的那样。清单 12-25 包含OrderCreatedHandler的实现。
Create a class called OrderCreatedHandler with a single Handle() method that takes an instance of the OrderCreated event you just created inside this project. To keep things simple, you can put this class just below the Program class in the same file, as you just did with the OrderCreated class. Listing 12-25 contains the implementation of the OrderCreatedHandler.
现在您已准备好告诉 Mass Transit 如何将您的事件和处理程序组合在一起。
Now you’re ready to tell Mass Transit how to put your event and handler together.
使用 Mass Transit 添加事件订阅的工作并不多;只需更新代码配置,告知 Mass Transit 需要监视哪个队列的消息以及如何处理它们。清单 12-26 是清单 12-23 中总线初始化的更新版本,它为OrderPlaced事件设置了一个订阅。
There’s not much work to adding event subscriptions with Mass Transit; just update the code-only configuration to inform Mass Transit which queue to watch for messages and what do with them. Listing 12-26, which is an updated version of the bus initialization you saw in Listing 12-23, sets up a subscription for the OrderPlaced event.
第一行新代码指示 Mass Transit 查看应用程序运行所在机器(localhost)上的promotions.ordercreated队列。Mass Transit 为您创建此队列,您可以在 Visual Studio 的服务器资源管理器中检查它,就像您之前检查错误队列一样。当 Mass Transit 在该队列中找到一条消息时,它会查找对该消息类型的订阅。您可以在清单 12-26 中刚刚添加的其他新配置行中看到如何设置此类订阅来处理OrderCreated事件。所发生的一切是,在config.Subscribe()调用中,Mass Transit 被告知创建OrderPlacedHandler的新实例,并将从队列中取出的消息传递给其Handle方法。
The first new line instructs Mass Transit to look on the promotions.ordercreated queue on the machine the application is running on (localhost). Mass Transit creates this queue for you, and it can be inspected in Visual Studio’s Server Explorer in the same way you inspected the error queue previously. When Mass Transit finds a message on that queue, it looks for a subscription to the message’s type. You can see how such a subscription is being set up to handle OrderCreated events on the other new lines of configuration you just added in Listing 12-26. All that’s happening is that inside the config.Subscribe() invocation Mass Transit is told to create a new instance of the OrderPlacedHandler and pass the message it has taken off the queue to its Handle method.
现在到了最有意思的部分:实现消息桥。为此,在本例中,您将创建一个新的 NServiceBus 处理程序,该处理程序订阅 NServiceBus 事件,并将其推送到 Mass Transit 配置的接收队列中。想象一下,如果您不使用 Mass Transit、.NET 或 Windows,情况会怎样?这其实并不重要,因为您将消息放入了一个队列中,而另一个运行在任何运行时或操作系统上的消息框架可以接收消息。
Now is the interesting part: implementing the messaging bridge. To do that, in this example you create a new NServiceBus handler that subscribes to the NServiceBus event and pushes it onto the queue Mass Transit has been configured to receive from. Imagine if you were not using Mass Transit, .NET, or Windows. It doesn’t really matter, because you’re putting the message in a queue that another messaging framework, running on any run-time or operating system, can take messages off.
要开始构建消息桥,您需要在同一个解决方案中创建一个名为Promotions.LuckyWinner.LuckyWinnerSelected.Bridge的新 C# 类库,并创建一个OrderCreatedHandler,其中包含清单 12-27 中的内容。您需要使用包管理器导入 NServiceBus,配置消息约定,引用Sales.Messages ,并在App.config中设置订阅。(如果您需要有关如何执行这些步骤的提醒,请参阅本章前面的部分,或查看本章的代码示例)。
To start building your messaging bridge, you need to create a new C# class library inside the same solution called Promotions.LuckyWinner.LuckyWinnerSelected.Bridge with an OrderCreatedHandler that contains the content in Listing 12-27. You need to import NServiceBus using the package manager, configure message conventions, reference Sales.Messages, and set up the subscription in App.config. (Refer to earlier parts of the chapter if you need a reminder about how to carry out these steps, or check this chapter’s code samples).
如上所述,此处理程序订阅 NServiceBus 事件并将其放入 Mass Transit 配置为监视的队列中。有几个关键细节需要了解。首先,注意到没有对 Mass Transit 的引用吗?桥接器所做的只是使用System.Messaging.dll提供的 MSMQ 类(您需要添加对它的引用)将消息放入队列。要注意的第二个重要方面是消息的格式。Mass Transit 首先查找标头,指示消息中包含的内容类型。您可以看到它被设置为application/vnd.masstransit+xml,这告诉 Mass Transit 正文包含 XML。然后,Mass Transit 将消息正文视为 XML,期望它采用从ConvertToMassTransitXmlMessageFormat()返回的格式。完成所有这些后,您只需创建一个队列并向其发送消息,如Handle()的最后两行所示。
As mentioned, this handler subscribes to the NServiceBus event and puts it in the queue Mass Transit has been configured to watch. There are a few key details to understand. First, notice how there is no reference to Mass Transit? All the bridge does is use the MSMQ classes provided by the System.Messaging.dll (you need to add a reference to that) to put messages into a queue. The second big aspect to notice is the format of the messages. Mass Transit first looks for a header indicating what type of content is contained in the messages. You can see this being set to application/vnd.masstransit+xml, which tells Mass Transit the body contains XML. Mass Transit then treats the body of the message as XML, expecting it to be in the format that is returned from ConvertToMassTransitXmlMessageFormat(). Once all that is done, you just create a queue and send a message to it, as shown in the final two lines of Handle().
从这个例子中,你可以看到一些与消息桥相关的问题。要将消息推送到消息框架,你需要了解它期望的格式。你还需要深入几个抽象层次,亲自处理队列。了解消息框架的内部结构可能需要额外的工作,但这并不总是一件坏事,尤其是在你经常需要监控队列并检查一切是否按预期工作的情况下。
From this example, you can see a few of the concerns related to a messaging bridge. To push a message into a messaging framework, you need to understand what format it expects. You also need to go down a few levels of abstraction and deal with the queues yourself. Understanding the internals of your messaging framework might be extra work, but it isn’t always a bad thing, especially when you often need to monitor the queues and check that everything is working as expected.
如果您发现在项目中构建消息传递桥效率低下,但仍希望使用一些响应式原则构建可扩展、容错的系统,那么 REST 可能更适合您。下一章将介绍 REST 和其他基于 HTTP 的解决方案。
If you find that building messaging bridges is becoming inefficient on a project but you still want to build scalable, fault-tolerant systems using some reactive principles, REST may be better. REST and other HTTP-based solutions are covered in the next chapter.
使用 Mass Transit 发布事件与使用 NServiceBus 发布事件几乎相同,如以下代码片段所示:
Publishing events with Mass Transit is almost identical to publishing events with NServiceBus, as shown in the following snippet:
Bus.Instance.Publish(new LuckyWinnerSelected(UserId ="user123"));
Bus.Instance.Publish(new LuckyWinnerSelected(UserId ="user123"));
作为练习,看看您是否可以使用 Mass Transit 构建一个新组件来处理由Promotions.LuckyWinner.LuckyWinnerSelected组件发布的事件。
As an exercise, see if you can build a new component with Mass Transit that handles events published by the Promotions.LuckyWinner.LuckyWinnerSelected component.
您的消息桥现在应该可以正常工作了,因此新的 Promotions 限界上下文应该能够处理OrderCreated事件。您只需要提供相关证明。要测试应用程序,请执行以下步骤:
Your messaging bridge should now be working, so your new Promotions bounded context should be able to handle OrderCreated events. All you need is proof of this. To test the application, carry out the following few steps:
图 12.30公共交通通过桥梁接收信息。
FIGURE 12.30 Mass Transit receiving messages via the bridge.
Mass Transit 包含许多本章未展示的实用功能。例如,它可以与其他队列技术(例如 Rabbit MQ)一起使用。Mass Transit 也拥有强大的社区。因此,如果您想了解更多信息,可以查看 GitHub 上的所有源代码(https://github.com/phatboyg/MassTransit),阅读官方文档(http://masstransit-project.com/ ),或者在 Mass Transit 邮件列表( https://groups.google.com/forum/#!forum/masstransit-discuss )中阅读并提问。
Mass Transit contains lots of useful functionality that was not shown in this chapter. For example, it can be used with other queuing technologies such as Rabbit MQ. Mass Transit also has a strong community. So if you’re keen to learn more about it, you can view all of the source code on GitHub (https://github.com/phatboyg/MassTransit), you can read the official documentation (http://masstransit-project.com/), or you can read and post questions on the Mass Transit mailing list (https://groups.google.com/forum/#!forum/masstransit-discuss).
本章内容是什么?
WHAT’S IN THIS CHAPTER?
Wrox.com 本章代码下载
Wrox.com Code Downloads for this This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 13 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 13 download and individually named according to the names throughout the chapter.
超文本传输协议 (HTTP) 是一种无处不在的协议,数十亿连接到互联网的设备都能理解它。它也是集成有界上下文的明智之选。HTTP 的广泛应用已充分证明,它能够使运行在不同硬件和软件栈上的应用程序相对轻松地进行通信。这意味着,如果您拥有使用不同技术的有界上下文,HTTP 会非常有吸引力。您在上一章中看到,虽然可以集成不同的消息传递框架,但其中可能涉及大量风险高且耗时的工作。但由于 HTTP 是一个众所周知的标准,因此在集成时您可以遵循现有的约定。
Hypertext Transport Protocol (HTTP) is a ubiquitous protocol that the billions of devices connected to the Internet understand. It can also be a discerning choice for integrating bounded contexts. Being so widespread, HTTP has clearly proven that it enables applications running on different hardware and software stacks to communicate relatively easily. This means that if you have bounded contexts using different technologies, HTTP can be very appealing. You saw in the previous chapter that although it is possible to integrate different messaging frameworks, there can be a lot of risky and time-consuming work involved. But because HTTP is a well-known standard, you can follow existing conventions when integrating with it.
第 11 章“有界上下文集成简介”和第 12 章“通过消息传递集成”向您展示了集成有界上下文不仅仅是为了让软件应用程序相互通信,它还提供了可扩展性和容错能力。您可能想知道,如果 HTTP 能够满足这些需求,为什么会有这么多的消息传递框架和中间件解决方案。这个问题很难给出确切的答案,但它确实凸显了 HTTP 在构建事件驱动的分布式系统时经常被忽视的事实。然而,REST 作为构建分布式系统的一种选择,无疑正开始变得越来越流行,它绝对是您至少应该考虑的一个选项。本章将向您展示其中的原因。
Chapters 11, “Introduction to Bounded Context Integration,” and 12, “Integrating via Messaging,” showed you that integrating bounded contexts is not just about making software applications talk to each other. It’s also about providing scalability and fault tolerance. You may be wondering why there are so many messaging frameworks and middleware solutions if HTTP satisfies these needs. That’s difficult to answer with any certainty, but it does highlight the fact that HTTP is often overlooked when building event-driven distributed systems. However, REST is definitely starting to gain popularity as a choice for building distributed systems, and it is definitely an option you should at least consider. This chapter shows you why.
虽然 HTTP 传统上并未用于构建响应式、事件驱动的系统,但它在使用远程过程调用 (RPC) 集成应用程序方面却广受欢迎。与 REST 相比,使用 RPC 进行集成的应用程序示例数以千计。这意味着有大量的现实世界证据表明了 RPC 的优缺点。您在第 11 章中了解了其中一些,在本章中,您将通过构建和比较 RPC 和 RESTful 领域驱动设计 (DDD) 系统来看到具体的示例。
Although HTTP hasn’t traditionally been used to build reactive, event-driven systems, it has been massively popular for integrating applications using remote procedure call (RPC). In contrast to REST, there are thousands of examples of applications that use RPC to integrate. This means there’s a lot of real-world evidence showing its strengths and weaknesses. You learned about some of them in Chapter 11, and in this chapter, you will see concrete examples by building and comparing RPC and RESTful Domain-Driven Design (DDD) systems.
无论你选择哪种基于 HTTP 的解决方案,都有一些模式和原则可以与 DDD 技术协同作用,使领域概念清晰化。例如,第 11 章和第 12 章演示了如何迁移到基于领域事件的事件驱动架构,从而带来业务和技术上的优势。你将在本章中看到,使用领域事件作为通过 REST 在有界上下文之间发送的消息,同样可以带来极佳的表达能力。
Whichever HTTP-based solution you choose, there are patterns and principles that synergize with DDD techniques to make domain concepts explicit. For example, Chapters 11 and 12 demonstrated how moving to an event-driven architecture based on domain events can have business and technical benefits. You’ll see in this chapter that using domain events as the messages sent between bounded contexts via REST can again be very expressive.
然而,事件并不总是以 HTTP 应用程序编程接口 (API) 格式呈现,尤其是在将您的域名作为 API 暴露给外部服务时。假设有一个 API 暴露了产品目录:网站不需要完整的事件历史记录,他们只想查看显示最新信息的最新快照。因此,本章还包含一些示例,用于将非事件的域名概念以 HTTP API 的形式暴露出来。
Events don’t always make sense as HTPP application programming interface (API) formats, though, especially when exposing your domain as APIs to external services. Consider an API that exposes a catalog of products: websites don’t want a full history of events, they just want to see the latest snapshot showing the most up-to-date information. So this chapter also contains examples for exposing domain concepts that aren’t events as HTTP APIs.
本章的最后一个主题是如何使松散耦合的团队能够高效地迭代其有界上下文。第 11 章展示了如何将面向服务架构 (SOA) 等概念应用于总体上支持这一需求,第 12 章展示了如何将这些概念应用于消息传递架构。本章提供了类似的指导,用于在使用 HTTP 作为集成协议时实现松散耦合的团队。
A final topic presented in this chapter is enabling loosely coupled teams to efficiently iterate their bounded contexts. Chapter 11 showed how concepts like Service Oriented Architecture (SOA) support this need in general, and Chapter 12 showed how to apply those concepts to messaging architecture. This chapter provides similar guidance for achieving loosely coupled teams when using HTTP as your integration protocol.
当全世界都在他们拥有的五种设备上使用 HTTP 时,它必然有其积极意义。以下是一些将有界上下文与 HTTP 集成可能会对您参与的项目的成功产生重大影响的原因。
When the whole world is using HTTP on all five of the devices they own, it must have its positives. Here are a few of the reasons why integrating your bounded contexts with HTTP might make a significant difference to the success of the projects you are involved in.
由于 HTTP 是一种平台无关的协议,因此每个与 HTTP 集成的应用程序或组件都可以使用任何技术构建。这不仅有利于创建松耦合的应用程序,而且还有助于创建彼此之间依赖关系较少的松耦合团队。
Each application or component that integrates with HTTP may be built using any technology thanks to HTTP being a platform-agnostic protocol. Not only is this beneficial for creating loosely coupled applications, but it can help to create loosely coupled teams that have few dependencies on each other.
使用 HTTP 时,每个有界上下文都必须遵守其公共契约,即 HTTP 请求和响应格式。只要遵守这些契约,团队就可以自由地进行重构,使用新技术重写应用程序,或按照自己的节奏继续增加业务价值。重要的是,他们必须遵守公共契约,以确保与其他有界上下文的集成保持完整。
Using HTTP, each bounded context must honor its public contracts, which are the HTTP request and response formats. Providing the contracts are adhered to, teams are then free to mercilessly refactor, rewrite their applications in new technologies, or continue to add business value at their own pace. All that matters is that they honor their public contract so that integration with other bounded contexts remains intact.
HTTP 无处不在。几乎所有编程语言和运行时都拥有丰富的库和对 HTTP 的支持。因此,在集成 HTTP 时,您可以获得大量的支持。在本章中,您将了解到 .NET 拥有许多用于构建基于 HTTP 的集成的框架,包括 Windows Communication Foundation (WCF) 和 ASP.NET Web API。
HTTP is everywhere. Almost all programming languages and run times have a wealth of libraries and support for using HTTP. So when integrating with HTTP, there is a massive amount of support available to you. In this chapter, you learn that .NET has a number of frameworks, including Windows Communication Foundation (WCF) and ASP.NET Web API, for building HTTP-based integrations.
当你需要扩充团队时,每个人都了解 HTTP 的另一个好处是。找到一位了解 HTTP 的开发人员很容易,但找到一位了解消息传递系统或特定消息传递框架的开发人员通常更具挑战性。
Another advantage to everyone understanding HTTP comes when you need to expand your team. It’s easy to find a developer who understands HTTP, but it can often be more challenging to find a developer who understands messaging systems or particular messaging frameworks.
除了用于构建基于 HTTP 的集成的现代框架和库之外,还有一些先进的工具。例如,当您将 Visual Studio 指向特定类型的 Web 服务时,它会为您生成类。这些类提供模拟 HTTP Web 服务 API 的方法,因此您可以编写看似标准的面向对象代码,但实际上却是通过网络进行通信的。稍后的 WCF 示例将演示这一点。
On top of the modern frameworks and libraries for building HTTP-based integrations, some tooling is advanced. One example of this is the way Visual Studio generates classes for you when you point it to a particular type of web service. The classes provide methods that mimic the API of the HTTP web services, so you can write what appears to be standard object-oriented code yet is actually communicating across the network. This is demonstrated later in the WCF examples.
当所有通信都通过 HTTP 进行时,可能不需要专门的通道来进行内部和外部通信。简而言之,这意味着您可以构建 API,有界上下文用于通信,第三方可以使用相同的 API。相比之下,消息传递系统几乎总是仅供内部使用,因此也必须生成 API。
When all your communication is over HTTP, there may be no need to have dedicated channels for internal and external communication. In plain English, this means that you can build APIs that bounded contexts use to communicate, and third parties can use those same APIs. In contrast, messaging systems are almost always for internal use only, so APIs have to be produced as well.
在内部使用与客户和合作伙伴共享的 API 的做法称为内部测试 (dogfooding)。内部测试非常可取,因为它可以帮助您获得与客户相同的体验。如果您的 API 包含一些让客户感到厌烦的痛点,内部测试或许能帮助您找到并消除这些痛点。当然,在某些情况下,内部测试也有缺点,可能并非最佳方法。例如,当您需要在内部而非外部提供更强大的性能保证时。
The practice of using the APIs internally that you share with clients and partners is known as dogfooding. Dogfooding is desirable because it helps you get the same experience as your customers. If your API contains pain points that are putting customers off, dogfooding might help you find and remove them. Of course, in some situations, dogfooding has drawbacks and might not be the best approach. One example might be when you need to have stronger performance guarantees internally than externally.
如果要构建与 HTTP 集成的分布式系统,一种选择是使用 RPC。正如第 11 章所述,当开发速度很重要或对可扩展性要求不太高时,RPC 的“隐藏网络”抽象会很有用。另一方面,RPC 固有的紧耦合性会使可扩展性要求和松耦合团队更难实现。
If you want to build distributed systems that integrate with HTTP, one option is to use RPC. As discussed in Chapter 11, RPC’s “hide-the-network” abstraction can be useful when development speed is important or scalability needs are not too high. On the other hand, the inherent tight coupling associated with RPC can make scalability requirements and loosely coupled teams harder to achieve.
以下部分将通过示例来说明前面提到的 RPC 的优点和缺点。它们可以帮助您形成自己的观点,并了解未来可能在哪些方面使用 RPC。
In the following section are examples that demonstrate the previously mentioned strengths and weaknesses of RPC. They allow you to start forming your own opinions and get a feel for where you might want to use RPC in the future.
在实现基于 HTTP 的 RPC 时,您有几种选择。传统的选择是使用一种名为 SOAP(简单对象访问协议)的协议,它在 HTTP 之上添加了另一层。然而,近年来,SOAP 的流行度大幅下降。如今,更现代的方法是简单地使用简单的可扩展标记语言 (XML) 或 JavaScript 对象表示法 (JSON) 作为 RPC 调用中的有效负载。为了帮助您做出明智的决定,本节将介绍这两种选择,首先介绍 SOAP。
You have a few choices when it comes to implementing RPC over HTTP. The traditional choice has been to use a protocol called SOAP (Simple Object Access Protocol), which adds another layer on top of HTTP. In recent years, however, SOAP has seen a massive decline in popularity. Nowadays, the more modern approach is to simply use plain eXtensible Markup Language (XML) or JavaScript Object Notation (JSON) as the payload in an RPC call. So that you can make informed decisions, you’ll see both options in this section, starting with SOAP.
SOAP 通过在有效负载中包含丰富的信息(例如类型和函数元数据)来完全拥抱 RPC 的概念。这使得将 SOAP 消息的内容转换为远程接收方的方法调用变得轻而易举。由于 SOAP 消息的丰富性以及在上一代开发者中的广泛应用,SOAP 的工具支持也得到了进一步的提升,您稍后会看到这一点。
SOAP fully embraces the concept of RPC by including rich information in the payload, such as type and function meta data. This makes it easy to convert the contents of a SOAP message into a method call on the remote receiver. Due to this richness and being massively popular with the previous generation of developers, the tooling support for SOAP is advanced, as you’ll see shortly.
为了了解 SOAP 并体验围绕它构建的高级工具,本节将集成两个构成社交媒体应用程序的有界上下文。在本场景中,假设您是一家初创公司的成员,正在构建一款类似 Twitter 的产品,该产品在市场上越来越受欢迎,用户群也在迅速增长。
To learn about SOAP and play with the advanced tooling that has been built around it, in this section you integrate two bounded contexts that form part of a social media application. For this scenario, envision that you are part of a start-up building a Twitter-like product that’s gaining traction in the market and a rapidly increasing user base.
在本例中,您将使用 RPC 来帮助开发团队加快开发速度。目前,他们有一个单体式的“大泥球”(BBoM)应用程序,其中的有界上下文仅仅是相互之间存在二进制依赖关系的库。当一个有界上下文的更改影响到其他有界上下文时,问题会波及整个业务。您将通过将每个有界上下文隔离为独立的应用程序来解决这个问题,这些应用程序只能通过 HTTP 进行通信。为了使这种转换尽可能无缝且快速,您将使用 RPC 将从一个有界上下文到另一个有界上下文的进程内方法调用替换为通过网络进行的 RPC(完全消除二进制依赖)。这将演示 RPC 如何几乎无需修改代码,并且几乎不影响网络。
For this example, you are going to use RPC to help the development teams move faster. Currently they have a single, monolithic, Big Ball of Mud (BBoM) application where bounded contexts are merely libraries that have a binary dependency on each other. This is causing problems to ripple across the entire business when changes to one bounded context are breaking others. You’re going to remove this problem by isolating each bounded context as a standalone application that can only be communicated with over HTTP. To make this transition as seamless and rapid as possible, you’ll use RPC to replace in-process method calls from one bounded context to another with RPCs over the network (completely removing the binary dependency). This will demonstrate how RPC requires few changes to your code and makes the network almost invisible.
RPC 的设计涉及确定哪些方法调用将通过网络进行 RPC。除此之外,您的代码看起来与在单机上运行基本相同。图 13.1展示了当前场景的新设计。请注意,它如何将两个有界上下文之间的方法替换为通过网络的 RPC。
Designing for RPC involves deciding which method calls will be RPCs across the network. Apart from that, your code will mostly look the same as it did running on a single machine. Figure 13.1 shows the new design for the current scenario. Note how it replaces methods between two bounded contexts with RPCs across the network.
图 13.1 “查找推荐用户”用例。
FIGURE 13.1 The “find recommended users” use case.
在(虚构的)当前系统中,Discovery 限界上下文正在调用FollowerDirectory类的FindUsersFollowers()方法,而该类属于 Account Management 限界上下文。这是一个需要进程内通信的二进制依赖关系。代码示例 13-1 演示了这一点。
In the (fictitious) current system, the Discovery bounded context is calling FindUsersFollowers() on the FollowerDirectory class, which belongs to the Account Management bounded context. This is a binary dependency that requires in-process communication. You can see the code demonstrating this in Listing 13-1.
清单 13-1 展示了被调用的FollowerDirectory.FindUsersFollowers()方法。这个方法存在问题,它耦合了两个有界上下文。我们将用一个跨网络的类似名称的 RPC 来替换它,从而消除 Discovery 和 Account Management 有界上下文之间有问题的二进制依赖关系。
Listing 13-1 shows the FollowerDirectory.FindUsersFollowers() method being called. This is the problematic method that couples two bounded contexts. It is going to be replaced with a similarly named RPC across the network, thereby removing the problematic binary dependency between the Discovery and Account Management bounded contexts.
图 13.1还提供了您将要实现的用例的更多背景信息。您可以看到,当用户登录并到达其主页时,该用例会被触发。此时,业务需求是让用户看到他们可能想要关注的推荐用户列表。这对业务至关重要,因为它允许用户发现其他用户和热门话题,从而吸引他们继续访问网站。一个由业务人员和开发人员组成的完整团队致力于帮助用户发现内容。这个团队被称为发现团队,而发现限界上下文代表了其领域范围。
Figure 13.1 also provides further background for the use case you are going to implement. You can see that the use case is triggered when a user logs in and arrives at her home page. When this happens, the business requirement is for users to see a list of recommended users whom they might want to follow. This is important to the business because it allows users to discover other users and hot topics so they continue to return to the site. An entire team of business people and developers is focused on helping users discover content. The team is known as the Discovery team, and the Discovery bounded context represents its area of the domain.
使用 .NET 的 Windows Communication Foundation (WCF) 时,使用 SOAP 集成两个有界上下文最初可能非常快速且相对容易。在后续章节中,当您开始实现图 13.1所示的用例时,您将亲身体验到这一点。
Integrating two bounded contexts using SOAP can initially be quite fast and relatively easy when using .NET’s Windows Communication Foundation (WCF). You’ll see this firsthand as you start to implement the use case shown in Figure 13.1 in the following sections.
首先,您需要创建一个空白的 Visual Studio 解决方案,用于存放所有有界上下文(在本 SOAP 示例中)。您可以将此解决方案命名为PPPDDD.SOAP.SocialMedia。需要创建的第一个有界上下文是 Account Management 。要添加 Account Management 有界上下文,您需要在项目中添加一个名为AccountManagement的新 WCF 服务应用程序,如图 13.2所示。
To get started, you need to create a blank Visual Studio solution that will be home to all the bounded contexts (in this SOAP example). You can call this solution PPPDDD.SOAP.SocialMedia. The first bounded context to be created is Account Management. To add the Account Management bounded context, you need to add a new WCF Service Application to the project called AccountManagement, as shown in Figure 13.2.
图 13.2添加帐户管理 WCF 服务。
FIGURE 13.2 Adding the Account Management WCF Service.
在旧的单体应用程序中,如清单 13-1 所示,有一个名为FollowerDirectory的类,它有一个名为FindUsersFollowers的方法。为了将这个进行 RPC 调用时,您只需在项目根目录下添加一个名为FollowerDirectory的 WCF 服务即可。然后,您可以使用 WCF 服务契约来声明您的 RPC,如下一节所示。
In the old monolithic application, as you saw in Listing 13-1, there was a class called FollowerDirectory that had a method called FindUsersFollowers. To turn this into an RPC call, you can simply add a WCF Service to the root of the project called FollowerDirectory. Then you can use WCF Service contracts to declare your RPCs, as shown in the next section.
添加 WCF 服务后,项目根目录中会添加两个文件:FollowerDirectory.svc.cs和IFollowerDirectory.cs。后者是 Visual Studio 用于生成公共 SOAP 契约(使用 Web 服务描述语言,即 WSDL)的文件。前者是实现;您的自定义代码将放在其中,并在运行时进行 RPC 调用时运行。您很快就会看到它的实际效果,所以如果感觉不太明白也不用担心。
After you’ve added the WCF Service, two files are added to the root of the project: FollowerDirectory.svc.cs and IFollowerDirectory.cs. The latter is what Visual Studio uses to generate a public SOAP contract (using the Web Service Description Language, or WSDL). The former is the implementation; your custom code goes in it and is run when RPC calls are made at run time. You see this in action shortly, so don’t worry if it doesn’t make perfect sense.
WCF 有两个注解:ServiceContract和OperationContract。ServiceContract添加到接口中,表示该类包含可在网络上以 RPC 形式调用的方法。OperationContract则表示用ServiceContract修饰的接口上的哪些方法是 RPC。因此,要创建FollowerDirectory.FindUsersFollowers() RPC 调用,应该应用这两个属性,如清单 13-2 所示。
WCF has two annotations: ServiceContract and OperationContract. ServiceContract is added to an interface to signify that the class contains methods that can be called as RPCs across the network. OperationContract then signifies which methods on an interface decorated with ServiceContract are the RPCs. Therefore, to create the FollowerDirectory.FindUsersFollowers() RPC call, you should apply those two attributes, as shown in Listing 13-2.
清单 13-2 是 Visual Studio 和 WCF 生成 RPC 基础结构所需的全部内容。稍后您将看到 Visual Studio 会在 Web 服务的客户端上自动生成这些类的代理。这种“免费网络”正是 WCF 和 SOAP 如此吸引人的原因。
Listing 13-2 is all that Visual Studio and WCF requires from you to be able to generate the RPC infrastructure. You’ll see later that Visual Studio automatically generates proxies of these classes on clients of the web service. This “networking for free” is what makes WCF and SOAP so appealing to many.
在你的服务运行之前,你需要在FollowerDirectory.svc.cs文件中提供一个实现。你可以在清单 13-3 中看到一个基本实现,它在内存中生成一些虚拟的 Follower 并返回它们。你可以使用此实现更新你的FollowerDirectory。
Before your service will work, you need to provide an implementation in the FollowerDirectory.svc.cs file. You can see a basic implementation in Listing 13-3 that generates a few dummy Followers in-memory and returns them. You can update your FollowerDirectory with this implementation.
现在,您可以测试是否真的可以通过网络以 RPC 方式调用FindUsersFollowers()了。Visual Studio 提供的测试客户端让测试变得简单。要运行测试客户端,请在解决方案资源管理器中选中FollowerDirectory.svc项(而不是FollowerDirectory.sv.cs ),然后按 F5。您将看到如图 13.3所示的测试客户端。
You’re now in a position to test that you really can call FindUsersFollowers()over the network as an RPC. Visual Studio makes this easy with the test client it provides. To run the test client, highlight the FollowerDirectory.svc item in the Solution Explorer (not FollowerDirectory.sv.cs) and press F5. You will then see the test client as illustrated in Figure 13.3.
图 13.3 Visual Studio 的 WCF 测试客户端。
FIGURE 13.3 Visual Studio’s WCF test client.
要测试您的新服务,只需在左侧资源管理器窗格中双击其名称(FindUsersFollowers ),然后在右侧窗格中accountId 行的Value列中输入一个值。完成后,如果您点击 Invoke 按钮,您的FindUsersFollowers RPC 将通过网络执行,结果将显示在右侧窗格的下半部分,如图13.4所示。
To test your new service, just double-click its name (FindUsersFollowers) in the left-hand Explorer pane, and then enter a value in the Value column for the row accountId in the right pane. After doing that, if you click the Invoke button, your FindUsersFollowers RPC will be carried out over the network, and the results will be displayed in the lower half of the right pane, as shown in Figure 13.4.
图 13.4在 WCF 的测试客户端中调用 RPC。
FIGURE 13.4 Invoking an RPC in WCF’s test client.
如果您想查看数据是如何在网络上传输的,可以点击右侧窗格底部的“XML”选项卡。这样,您就可以看到原始的 SOAP:
If you want to see how the data was transmitted across the network, you can click on the XML tab at the bottom of the right pane. By doing that, you see the raw SOAP:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header />
<s:Body>
<FindUsersFollowersResponse>
<FindUsersFollowersResult
xmlns:a="http://schemas.datacontract.org/2004/07/
AccountManagement" xmlns:i="http://www.w3.org/2001/
XMLSchema-instance">
<a:Follower>
<a:FollowerId>follower_0</a:FollowerId>
<a:FollowerName>happy follower 0</a:FollowerName>
<a:SocialTags
xmlns:b="http://schemas.microsoft.com/2003/10/
Serialization/Arrays">
<b:string>programming</b:string>
<b:string>DDD</b:string>
<b:string>Psychology</b:string>
</a:SocialTags>
</a:Follower>
<a:Follower>
...
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header />
<s:Body>
<FindUsersFollowersResponse>
<FindUsersFollowersResult
xmlns:a="http://schemas.datacontract.org/2004/07/
AccountManagement" xmlns:i="http://www.w3.org/2001/
XMLSchema-instance">
<a:Follower>
<a:FollowerId>follower_0</a:FollowerId>
<a:FollowerName>happy follower 0</a:FollowerName>
<a:SocialTags
xmlns:b="http://schemas.microsoft.com/2003/10/
Serialization/Arrays">
<b:string>programming</b:string>
<b:string>DDD</b:string>
<b:string>Psychology</b:string>
</a:SocialTags>
</a:Follower>
<a:Follower>
...
现在您将看到 WCF、SOAP 和 Visual Studio 的组合为您完成了许多繁琐的工作。您将首先创建 Discovery 限界上下文项目,然后了解如何通过将 Discovery 限界上下文指向 Account Management 限界上下文的统一资源定位符 (URL) 来开始对 Account Management 限界上下文进行 RPC 调用。
You’re now about to see that the WCF, SOAP, and Visual Studio combination does a lot of the hard work for you. You’ll first create the Discovery bounded context project, and you’ll then see how you can start making RPC calls to the Account Management bounded context just by pointing the Discovery bounded context to the uniform resource locator (URL) of the Account Management bounded context.
首先,您需要向解决方案中添加一个名为Discovery 的新 WCF 服务应用程序,该应用程序代表 Discovery 限界上下文。然后,您需要在 Discovery 限界上下文中添加一个名为 Recommender 的 WCF 服务。Recommender提供网站用来获取推荐用户列表的 Web 服务。快速参考图 13.1中的设计可能会有所帮助。
To begin, you need to add a new WCF Service Application to the solution called Discovery that represents the Discovery bounded context. Inside the Discovery bounded context, you then need to add a WCF Service called Recommender. Recommender provides the web services that the website uses to get the list of recommended users. You may find it helpful to quickly refer to the design in Figure 13.1.
推荐器有两个职责。首先,它为客户端提供请求推荐的 API。为了履行这一职责,它会向账户管理限界上下文发起 RPC 调用,以获取账户的关注者。要实现这一点,您需要运行账户管理项目。(在解决方案资源管理器中选中它,然后按 Ctrl+F5。)然后,您可以通过在 Web 浏览器中直接访问http://localhost:3100/FollowerDirectory.svc来测试它是否正在运行。如果页面标题为“FollowerDirectory Service”,则表示一切正常。
Recommender has two responsibilities. First, it provides the API for clients to request recommendations. To fulfill that responsibility, it makes the RPC to the Account Management bounded context get an Account’s followers. To implement that, you need to have the Account Management project running. (Highlight it in the Solution Explorer and press Ctrl+F5.) You can then test that it is running by directly accessing it in a web browser at http://localhost:3100/FollowerDirectory.svc. If the page has the heading “FollowerDirectory Service,” things are working.
接下来,您需要将 URL 传递给 Visual Studio,以便它生成代理类。在解决方案资源管理器中,右键单击Discovery 项目的“引用”节点,然后选择“添加服务引用”,即可将 URL 粘贴到“地址”字段中。剩下的就是将“命名空间”(位于“添加服务引用”对话框的底部)更改为AccountManagement,然后单击“执行”。您的屏幕之后应该类似于图 13.5所示,其中展开了FollowerDirectory节点,并显示了它所识别的 Web 服务。确认无误后,单击“确定”。
Next, you need to pass the URL to Visual Studio so it can generate the proxy classes. If you right-click on the References node for the Discovery project in the Solution Explorer and select Add Service Reference, the URL can be pasted into the Address field. All that’s left is to change the Namespace (at the bottom of the Add Service Reference dialog) to AccountManagement and click Go. Your screen should then resemble Figure 13.5, which shows the expanded FollowerDirectory node revealing the web service it has identified. Once you’re happy, you can click OK.
图 13.5在 Visual Studio 中添加服务引用。
FIGURE 13.5 Adding a Service Reference in Visual Studio.
要查看生成的代理类,可以检查添加到“服务引用”文件夹中的AccountManagement项。图 13.6展示了生成的代理类。这些类稍后将在 Discovery 限界上下文中实例化,并调用其方法(通过 SOAP/HTTP)来调用与 Account Management 限界上下文相关的 RPC。
To see the generated proxy classes, you can inspect the AccountManagement item that was added to the Service References folder. Figure 13.6 shows the generated proxy classes. These are the classes that you will shortly instantiate in the Discovery bounded context and call methods on to invoke RPCs across to the Account Management bounded context (via SOAP/HTTP).
图 13.6生成的代理类。
FIGURE 13.6 Generated proxy classes.
最后一步是构建 Recommender Web 服务,使生成的代理类能够正常工作。清单 13-4 和清单 13-5 展示了IRecommender和Recommender的基本实现,它们足以演示 RPC 调用。您需要将它们添加到Discovery项目中。
The last step is to build the Recommender web service that puts the generated proxy classes to work. Listing 13-4 and Listing 13-5 show a basic implementation of the IRecommender and Recommender that do just enough to demonstrate the RPC call. You need to add these to your Discovery project.
在清单 13-5 中,创建了AccountManagement.FollowerDirectoryClient的一个实例。它是 Visual Studio 在添加服务引用时生成的代理类。当调用它的FindUsersFollowers()方法时,它会通过网络触发一个 RPC,并调用您在帐户管理限界上下文中添加的代码。这里的主要要点是 WCF 和 Visual Studio 负责所有与网络相关的管道。即使不涉及网络,您添加的大多数代码看起来也非常相似。
In Listing 13-5, an instance of AccountManagement.FollowerDirectoryClient is created. It is a proxy class that Visual Studio generated when adding the Service Reference. When its FindUsersFollowers() method is called, it fires an RPC across the network and calls into the code you added in the Account Management bounded context. The main takeaway here is that WCF and Visual Studio took care of all the network-related plumbing. Most of the code you added would look very similar even if there was no network involved.
您可以通过将两个项目设置为启动来测试一切是否正常(如上一章所示,在解决方案资源管理器中右键单击解决方案并选择“设置启动项目”),然后在解决方案资源管理器中的Recommender.svc文件上按 F5 键。WCF 测试客户端会再次弹出,这次您需要调用GetRecommendedUsers() ,如图13.7所示。
You can test that everything is successfully working by setting both projects to start up (as shown in the previous chapter by right-clicking the solution in the Solution Explorer and choosing Set Startup Projects) and then pressing F5 on the Recommender.svc in the Solution Explorer. The WCF test client pops up again, and this time you need to invoke GetRecommendedUsers(), as demonstrated in Figure 13.7.
图 13.7 RPC 一定已经发生。
FIGURE 13.7 The RPC must have occurred.
图 13.7中 RPC 调用的结果是由您创建的 Web 服务返回的硬编码数据。这表明两个有界上下文之间的 RPC 调用已成功完成。总而言之,本示例的目标——即在不产生二进制依赖的情况下集成账户管理和发现有界上下文——已经实现。
The result of the RPC call in Figure 13.7 is the hard-coded data that is returned by the web service you created. This indicates that the RPC call between the two bounded contexts successfully occurred. Conclusively, this example’s aim of integrating the Account Management and Discovery bounded contexts without a binary dependency has now been achieved.
虽然目前有很多公共的 SOAP API,但现在已经不再使用 SOAP 构建新的 API。SOAP 的一大痛点是其消息格式的复杂性和冗长性。您之前已经略微了解过这一点。人们批评不必要的复杂性,并且经常引用 SOAP 作为不必要复杂性的完美例子。因此,您应该谨慎公开 SOAP API,但如果内部使用 SOAP 符合您的需求,则不必过于担心。
Although there are many existing public SOAP APIs, new APIs just aren’t being built with SOAP anymore. One of SOAP’s big pain points is the complexity and verbosity of its message format, which you saw a glimpse of earlier. People are critical of needless complexity, and they often cite SOAP as a perfect example of unnecessary complexity. Accordingly, you should be careful about exposing public SOAP APIs, but you shouldn’t feel too concerned about using SOAP internally if it suits your needs.
如今,RPC over HTTP 的主流做法是使用轻量级的纯 XML 或 JSON 负载。下一节将使用 ASP.NET Web API 展示此类示例。
The modern preference for RPC over HTTP is to use lightweight, plain XML or JSON payloads. The next section shows examples of this using ASP.NET Web API.
为了了解如何通过 HTTP 集成,避免 SOAP 格式的复杂性和冗长,本节将使用相对轻量级的 JSON 重新创建社交媒体 SOAP 集成。以下是之前显示的 SOAP 负载的 JSON 版本。您可以回过头来比较一下,以充分体会 JSON 的简洁性。
To see how you can integrate over HTTP without the complexity and verbosity of the SOAP format, in this section, you re-create the social media SOAP integration instead using the relatively lightweight JSON. The following is the JSON version of the SOAP payload shown earlier. You may want to go back and compare to fully appreciate how the JSON is far more compact.
{
"followers": [
{
"accountId":"34djdlfjk2j2",
"socialTags": [
"ddd","soa","tdd","kanban"
]
},
...
]
}
{
"followers": [
{
"accountId":"34djdlfjk2j2",
"socialTags": [
"ddd","soa","tdd","kanban"
]
},
...
]
}
ASP.NET Web API 是微软最新的 Web 服务创建框架。稍后您将了解它如何成为构建 RESTful API 的理想选择。但现在,您将首先了解它如何简化 JSON RPC API 的构建。首先,您需要一个名为PPPDDD.JSON.SocialMedia 的新空白 Visual Studio 解决方案。此解决方案需要填充一个名为AccountManagement的 ASP.NET Web 应用程序项目。在创建项目的过程中,您需要选择“空”模板并选中“Web API”复选框。创建完成后,您需要将项目配置为始终从端口 3200 启动。
The ASP.NET Web API is Microsoft’s latest framework for creating web services. Later you will see how it can be a good choice for building RESTful APIs. But for now you’ll see how it can make life easy when building JSON RPC APIs. As a starting point, you need a new blank Visual Studio solution called PPPDDD.JSON.SocialMedia. This solution needs to be populated with an ASP.NET Web Application project called AccountManagement. As you go through the creation process for the project, you need to select the Empty template and check the Web API check box. Once it’s created, you need to configure the project to always start on port 3200.
控制器包含向 Web API 发出 Web 请求时将运行的代码。您可以将控制器视为从通用语言 (UL) 表达领域概念的机会,但在将领域逻辑放入控制器时应谨慎。一些开发人员将控制器概念化为应用服务。
Controllers contain the code that will be run when web requests are made to your Web API. You can see controllers as an opportunity to express domain concepts from the ubiquitous language (UL), although you should be careful about putting domain logic in them. Some developers conceptualize controllers as Application Services.
要开始此示例,您需要在项目根目录中的Controllers文件夹中添加一个名为FollowerDirectoryController的类;Web API 要求将控制器放在此文件夹中。FollowerDirectoryController的代码如清单 13-6 所示。
To start with this example, you need to add a class called FollowerDirectoryController to the Controllers folder in the root of the project; Web API requires that controllers be placed in this folder. The code for FollowerDirectoryController is shown in Listing 13-6.
为了演示目的,清单 13-6 中的FollowerDirectoryController返回一个硬编码的Follower列表(以 JSON 格式)。在实际应用程序中,此类可能会执行数据库查找或 API 调用来获取所需的关注者信息。
For demonstrative purposes, the FollowerDirectoryController in Listing 13-6 returns a list of hard-coded Followers (as JSON). In a real application, this class would likely perform database lookups or API calls to get the required follower information.
启动应用程序(按 F5 键)后,您可以通过在浏览器中访问它来测试新的 Web 服务。使用 Web API 的默认约定,可以通过http://localhost:3200/api/followerdirectory/getusersfollowers?accountId=123访问它,其中accountId参数的值是变量。(在本例中,它可以是任意值。)图 13.8展示了一个从浏览器访问 API 并查看 JSON 响应的示例。
After starting the application (by pressing F5), you can test the new web service by hitting it in the browser. Using Web API’s default conventions, it is accessible at http://localhost:3200/api/followerdirectory/getusersfollowers?accountId=123, where the value for the accountId parameter is variable. (It can be anything in this example.) Figure 13.8 shows an example of hitting the API from a browser and viewing the JSON response.
图 13.8在浏览器中查看 Web API 控制器的输出。
FIGURE 13.8 Viewing the output of a Web API controller in a browser.
可以看出,与 SOAP 和 WCF 相比,这种方法需要额外做一些工作来设置控制器,但通过网络发送的数据更加简洁明了,更容易理解正在发生的事情。这对于调试问题来说也是一个优势。
You can see with this approach that you had to do a tiny bit of extra work setting up the controller compared to SOAP and WCF, but the data sent across the wire is so much cleaner and lighter that it’s easy to understand what is happening. This is also a bonus when it comes to debugging problems.
由于缺乏 SOAP 提供的丰富元数据,因此无法为纯 JSON API 自动生成代理类。不过,正如您现在将看到的,这仍然不需要太多额外的工作。
Lacking the richness of meta data provided by SOAP, it is not possible to automatically generate proxy classes for a plain JSON API. It still doesn’t have to be a lot of extra work, though, as you’ll now see.
要创建 JSON API 的客户端,需要添加一个名为Discovery的新 ASP.NET Web 应用程序来表示 Discovery 有界上下文。在Discovery项目内部,需要在Controllers文件夹中添加一个名为RecommenderController的类。该类的代码如清单 13-7 所示。为了进行编译,需要在 Nuget 包管理器控制台中运行以下命令来安装HttpClient和ServiceStack.Text :
To create a client of your JSON API, you need to add a new ASP.NET Web Application, called Discovery, to represent the Discovery bounded context. Inside the Discovery project, you need to add a class called RecommenderController in the Controllers folder. The code for this class is shown in Listing 13-7. For it to compile, you need to install HttpClient and ServiceStack.Text by running the following commands in the Nuget Package Manager Console:
Install-Package Microsoft.AspNet.WebApi.Client -Project Discovery
Install-Package ServiceStack.Text -Project Discovery
Install-Package Microsoft.AspNet.WebApi.Client -Project Discovery
Install-Package ServiceStack.Text -Project Discovery
如清单 13-7 所示,此实现的逻辑与前一个示例中的 WCF 方法类似。但是,使用此解决方案,您必须手动完成发出 HTTP 请求和解析响应的工作。不过,正如您所见,微软和社区提供了功能丰富的库,可以为您完成许多繁重的工作。
As Listing 13-7 shows, the logic for this implementation is similar to the WCF approach in the previous example. However, with this solution, you have to do the manual work of making the HTTP request and parsing the response yourself. As you can see, though, there are feature-rich libraries, provided by Microsoft and the community, that do a lot of the laborious work for you.
为了测试一切是否按预期运行,您需要将两个项目都设置为启动项目(如前面的示例所示)。然后,您需要导航到刚刚创建的GetRecommendedUsers API 的 URL。该 URL 为 http://localhost:{port}/api/recommender/getrecommendedusers?accountId=123,具体取决于 Discovery 限界上下文在您计算机上使用的端口。在解决方案中按下 F5 键时,会自动弹出一个浏览器,告知您端口号(或者,您也可以手动指定端口,如本章前面所示)。
To test that everything works as intended, you need to set both projects as start-up projects (as shown in previous examples). You then need to navigate to the URL of the GetRecommendedUsers API you just created. The URL is http://localhost:{port}/api/recommender/getrecommendedusers?accountId=123 depending on which port the Discovery bounded context is using on your machine. A browser automatically pops up informing you of the port number when you press F5 inside the solution (or you can manually specify a port, as shown earlier in this chapter).
刚刚演示了使用两种常见方法将有界上下文与 RPC over HTTP 集成。使用 WCF 和 SOAP,您可以向领域模型添加一些属性,它就会突然变成一个分布式系统,摆脱有界上下文之间的二进制依赖。反过来,这使得团队更加独立,不必担心破坏其他有界上下文。然而,SOAP 的一个问题是格式复杂且冗长;在许多情况下,RPC 仅用于简单的集成,因此这似乎不太合理。这就是为什么纯 XML 或 JSON 如今更受欢迎的原因。
Integrating bounded contexts with RPC over HTTP was just demonstrated using two common approaches. With WCF and SOAP you can add a few attributes to your domain model and suddenly it becomes a distributed system free from binary dependencies between bounded contexts. In turn, this allows teams to be more independent and not have to worry about breaking other bounded contexts. One problem with SOAP, though, is that the format is complex and verbose; in many cases, RPC is used for simple integrations, so this doesn’t seem logical. This is why plain XML or JSON is the more popular choice today.
然而,这两种方案都存在第 11 章中提到的 RPC 固有缺陷。首先,它们可能更难有效扩展。回顾图 13.1中的图表,假设业务部门即将提出一个请求,要求提高推荐关注者在屏幕上显示的速度。为了提升整体性能,发现限界上下文和账户管理限界上下文可能都需要扩展。如果 RPC 链跨越三个限界上下文,那么由于时间耦合,可能需要扩展这三个限界上下文。
Both options, however, have the flaws inherent to RPC that were mentioned in Chapter 11. First, they can be harder to scale efficiently. Looking back at the diagram in Figure 13.1, consider a forthcoming request from the business to improve the speed at which recommended followers are displayed onscreen. Both the Discovery bounded context and the Account Management bounded context may need to be scaled to provide an overall performance improvement. If the chain or RPCs spanned three bounded contexts, then three bounded contexts may need to be scaled due to the temporal coupling.
在容错方面,也存在一些令人担忧的迹象。如果账户管理有界上下文发生故障,发现有界上下文也会发生故障,因为它无法通过 RPC 跨域获取关注者。同样,这是由于时间耦合造成的。
In terms of fault tolerance, there are also worrying signs. If the Account Management bounded context goes down, the Discovery bounded context also goes down because it cannot RPC across and get the followers. Again, this is due to the temporal coupling.
你可能觉得需要在集成 HTTP 以实现松散的平台耦合,和使用 NServiceBus 等消息传递框架构建可扩展、容错的系统之间做出选择。但这完全不是事实。本章的下一部分将展示如何将响应式编程的原则与 REST 相结合,从而实现消息传递系统的可扩展性和容错性以及 HTTP 的松散平台耦合。
It may seem like you need to choose between integrating with HTTP for loose platform coupling and having a scalable, fault-tolerant system that uses a messaging framework like NServiceBus. But that’s completely untrue. The next section of this chapter shows that you can have the scalability and fault tolerance of a messaging system and the loose platform coupling of HTTP by combining the principles of reactive programming with REST.
在本节中,您将第三次重建社交媒体集成。不过,这一次,您将使用事件驱动的 REST 彻底重新设计,以实现可扩展性、容错性和开发效率。第三次设计迭代仍然仅依赖于 HTTP,而不是重量级消息传递框架。但此版本仍然遵循第 11 章中介绍的响应式、SOA 和松耦合原则。
In this section, you rebuild the social media integration for a third time. This time, however, you completely redesign for scalability, fault tolerance, and development efficiency using event-driven REST. This third design iteration still relies only on HTTP instead of a heavyweight messaging framework. But this version still follows the reactive, SOA, and loose coupling principles presented in Chapter 11.
然而,REST 是一个被误解和误用的术语。在构建任何 RESTful 应用程序之前,理解 REST 的真正含义至关重要。
REST is a misunderstood and misused term, though. Before you build any RESTful applications, it is crucial that you understand what REST really is.
REST 由 Roy Fielding 首次提出。他基于互联网成功的原则,创建了 REST 这一架构风格。REST 包含许多基本概念,包括资源和超媒体,它们为可演进的客户端和服务器提供了平台。
REST was introduced to the world by Roy Fielding. He created REST as an architectural style based on the principles that make the Internet so successful. REST has a number of fundamental concepts, including resources and hypermedia, which provide the platform for evolvable clients and servers.
RESTful 系统的 HTTP 请求用于获取资源。响应包含请求的资源(如果成功)。资源可以是文档(例如网页)或媒体(例如 MP3 文件)。资源与 DDD 完美契合,因为领域中的概念可以表示为资源,从而进一步扩展 UL。举个简单的例子,在金融领域,可能存在将资金从一个账户转移到另一个账户的交易。UL 会为每种类型的交易(例如 B2B 交易或个人交易)包含一个条目。这些交易可以作为资源公开,可通过统一资源标识符(URI,又称 URL)http://pppdddemo.com/B2bTransactions访问。 或者http://pppddddemo.com/PersonalTransactions。这与 RPC 完全不同,在 RPC 中,请求和响应使用命令式命名来模拟方法调用。
HTTP requests to a RESTful system are for resources. Responses contain the requested resource (if successful). Resources can be things like documents, such as web pages, or media, such as MP3 files. Resources work well with DDD because concepts in your domain can be expressed as resources—further spreading the UL. As a basic example, in a financial domain, there could be transactions that transfer funds from one account to another. The UL would contain an entry for each type of transaction, such as B2B Transaction or Personal Transaction. These transactions could be exposed as resources accessible from the uniform resource identifiers (URIs aka URLs) http://pppddddemo.com/B2bTransactions or http://pppddddemo.com/PersonalTransactions. This is completely different from RPC, where requests and responses simulate method calls using imperative naming.
资源与表示之间存在一对多关系。换句话说,请求资源时,您可以指定不同的协议或内容类型,例如 JSON、XML 或 HTML。每个响应都是相同的资源,但会根据请求格式的语法规则以不同的方式呈现。您很快就会看到,RESTful API 的客户端通过在 HTTP 的“Accept”标头中指定所需的多媒体编码 (MIME) 类型来选择格式。
Resources have a one-to-many relationship with representations. In other words, when requesting a resource, you can specify a different protocol or a different content type, such as JSON, XML, or HTML. Each response will be the same resource but will be presented differently according to the syntactic rules of the requested format. You will see shortly that clients of RESTful APIs choose a format by specifying the required Multi Media Encoding (MIME) type in HTTP’s “Accept” header.
以下是与资源相关的其他一些关键细节:
Here are a couple other key details relating to resources:
浏览网页的人通过点击链接从一个网页跳转到另一个网页。这就是超媒体的运作方式。通过返回资源中的超链接,计算机也可以简单地通过跟随链接从一个资源跳转到另一个资源。本章后面将演示这一点。
Humans browsing the web go from web page to web page by clicking links. This is hypermedia in action. By returning hyperlinks in resources, computers, too, can move from resource to resource simply by following links. This is demonstrated later in the chapter.
超媒体为 DDD 从业者提供了另一个机会,让他们能够更清晰地表达自己的领域。想象一下汽车保险单。申请流程的每个步骤都可以用超媒体的链接来表示,指向下一个可能的步骤——使用 UL 来表达。这不仅可以表达领域概念,还可以用于建模工作流或领域流程。
Hypermedia presents another opportunity for DDD practitioners to express their domain more explicitly. Imagine a car insurance policy. Each step of the application process could be expressed as links in hypermedia to the next possible steps—expressed using the UL. Not only does this express domain concepts, but it can be used to model workflows or domain processes.
在机器对机器通信中使用超媒体意味着 RESTful API 的客户端与其 URI 不再耦合。这使得客户端和服务器解耦,可以自由地独立发展。这是人们使用 REST 的根本原因之一,因为基于 SOAP 的解决方案由于客户端和服务器紧密耦合而往往很脆弱。
Using hypermedia in machine-to-machine communication means that clients of a RESTful API are not coupled to its URIs. This leads to decoupled clients and servers, free to evolve independently. This is one of the fundamental reasons people have for using REST, because SOAP-based solutions tend to be brittle due to tightly-coupled clients and servers.
在 RESTful 应用中,应用程序状态(例如用户购物车中的商品)似乎不应该存储在服务器上。这为容错和可扩展性奠定了基础,因为客户端不必一直访问包含状态的同一台机器。因此,应用程序状态应该保存在客户端,并在服务器每次需要时将其发送到服务器。回到购物车示例,在无状态 REST API 中,购物车商品可以存储在 Cookie 中,并随每次请求发送到服务器。因此,一台服务器可能出现的任何问题都不应妨碍其他服务器介入接管它。
Application state, such as the items in a user’s shopping cart, arguably should not be stored on the server in a RESTful application. This provides a foundation for fault tolerance and scalability because clients do not have to keep hitting the same machine that contains the state. Application state should therefore be kept on the client and sent to the server every time the server requires it. Going back to the shopping cart example, in a stateless REST API, the cart items could be stored in cookies and sent to the server with every request. Any problems a server may have, therefore, should not preclude other servers stepping in to take over from it.
HTTP 包含许多约定,这些约定为可扩展性、容错性和松耦合性提供了基础。由于 REST 正是基于这些使 Web 成功的原则,因此在构建 RESTful 应用程序之前,您至少需要对 HTTP 的功能有一个基本的了解。
HTTP has a number of conventions that provide the basis for scalability, fault tolerance, and loose coupling. Because REST is based on these principles that make the web successful, it is essential that you at least have a basic understanding of HTTP’s features before you build RESTful applications.
HTTP 提供了统一的资源交互接口。例如,要获取资源,您可以向其 URI 发送 GET 请求。要删除同一资源,您可以向该 URI 发送 DELETE 请求。您可以使用 PUT 请求在所需的 URI 上创建资源。要向集合中添加项目,您可以使用 POST 动词。表 13.1显示了如何使用这些常用动词与资源交互的示例。
HTTP provides a uniform interface for interacting with resources. For example, to fetch a resource, you send a GET request to its URI. To delete the same resource, you send a DELETE request to the URI. You can use PUT requests to create a resource at the desired URI. For adding items to a collection, you can use the POST verb. Examples of how these common verbs can be used to interact with resources are shown in Table 13.1.
表 13.1使用 HTTP 动词创建、读取、更新和删除资源
TABLE 13.1 Using HTTP Verbs to Create, Read, Update, and Delete Resources
| URI | 动词 | 行动 |
| /帐户/用户123 | 得到 | 读取/获取资源 |
| /帐户/用户123 | 删除 | 删除资源 |
| /帐户/用户123 | 放 | 创建资源 |
| /帐户/user123/地址 | 邮政 | 更新资源 |
通过在整个互联网上统一应用一组动词,可以轻松构建通用的 API 客户端和基础架构组件(例如缓存),这些组件能够理解 Web 的约定。想想看——每种编程语言都有处理 HTTP 的库。这就是通用约定的强大之处,您也可以在集成有界上下文时利用它们。
By having a single set of verbs that are applied uniformly across the entire Internet, it is easy to build generic API clients and infrastructure components, such as caches, that understand the web’s conventions. Think about it—every programming language has libraries for working with HTTP. This is the power of common conventions, and you can also harness them when integrating bounded contexts.
HTTP 的状态码与 HTTP 的动词相辅相成。与动词一样,拥有一套通用的状态码意味着网络上的任何代理都能理解这些约定。例如,每当你发出向不存在的 URI 发出请求,您将收到 HTTP 404 状态代码,因为这是几乎所有系统都遵守的通用标准。
Complementary to HTTP’s verbs are its status codes. As with verbs, having a common set of status codes means any agent on the web understands the conventions. For example, whenever you make a request to a URI that doesn’t exist, you get an HTTP 404 status code back because it is a common standard that almost all systems adhere to.
HTTP 状态码按其首位数字分组,如表 13.2所示。每组包含更具体的状态码。
HTTP status codes are grouped by their first digit, as shown in Table 13.2. Within each group are more specific status codes.
表 13.2 HTTP 状态代码组
TABLE 13.2 HTTP Status Code Groups
| 状态代码组 | 定义 | 例子 |
| 1xx | 信息 | 这很少被使用。 |
| 2xx | 成功 | 您请求的资源已返回。 |
| 3xx | 重定向 | 您请求的资源已被移至另一个地址。 |
| 4xx | 客户端错误 | 您提供的参数值无效。 |
| 5xx | 服务器错误 | API 代码中存在一个错误,导致无法返回资源。 |
如果您想了解更多,维基百科对 HTTP 状态代码有一个易于理解的介绍(http://en.wikipedia.org/wiki/List_of_HTTP_status_codes)。
Wikipedia has an accessible introduction to HTTP status codes if you would like to learn more (http://en.wikipedia.org/wiki/List_of_HTTP_status_codes).
除了 HTTP 请求/响应的 URI 和主体之外,你可能还熟悉提供额外信息的标头。RESTful 系统经常使用 HTTP 的缓存标头,本章稍后会介绍这些标头。
Aside from the URI and body of an HTTP request/response, you’re probably familiar with headers that provide extra information. RESTful systems frequently use HTTP’s caching headers that are covered later in this chapter.
大多数 RESTful 应用程序都需要一定程度的安全性。由于 REST 的一个特性是无状态,因此通常建议使用 OAuth 等协议在标头中传达身份验证和授权详细信息。
Most RESTful applications require some level of security. Because a property of REST is statelessness, it’s often recommended that authentication and authorization details are communicated in headers using protocols such as OAuth.
有关可以在 HTTP 请求和响应中使用的标头的更多信息,请参阅 Wikipedia 中一个易于理解且详细的条目(http://en.wikipedia.org/wiki/List_of_HTTP_header_fields)。
For more information on headers that you can use in HTTP requests and responses, Wikipedia has an accessible, yet detailed entry (http://en.wikipedia.org/wiki/List_of_HTTP_header_fields).
REST 对于许多项目来说可能是一个不错的选择,但对于其他项目来说可能并非最佳选择。无论你做出何种选择,为了确保沟通的准确性,恰当地命名至关重要。遗憾的是,REST 是一个被滥用的术语。因此,在将你的 API 称为 RESTful 之前,你应该至少检查一下它是否以超媒体和资源为中心。
REST can be a good choice for many projects and a suboptimal choice for others. Whatever choice you make, it’s important to name things accordingly for accurate communication. Unfortunately, REST is a much misused term. So before calling your API RESTful, you should check that as a bare minimum it centers on hypermedia and resources.
在 REST 一词被多次滥用之后,Roy Fielding 于 2008 年被迫撰写了一篇博客文章,要求人们满足一些基本要求,才能将其 API 称为 RESTful(http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven)。另一个应对 REST 术语滥用的措施是 Richardson 成熟度模型(http://martinfowler.com/articles/richardsonMaturityModel.html)。这实际上是一个衡量指标,可以显示您的 API 与 RESTful 的距离。
After numerous high-profile abuses of the term REST, in 2008 Roy Fielding was compelled to write a blog post demanding that people meet some basic requirements for their API to be called RESTful (http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven). Another countermeasure to the abuse of the term REST is the Richardson Maturity Model (http://martinfowler.com/articles/richardsonMaturityModel.html). This is basically a barometer indicating how close your API is to being RESTful.
本章剩余内容的背景是对前面 RPC 示例的重新设计。凭借一些杀手级新功能和病毒式营销活动,这家虚构的社交媒体初创公司的用户注册量再次呈指数级增长。该公司希望通过增加高级账户并将其推广给普通用户来获利。高级账户是公司在社交媒体网站上吸引粉丝的一种方式,从而通过提升品牌忠诚度获利。
The context for the remainder of this chapter is a redesign of the RPC examples earlier in the chapter. With some killer new features and viral marketing campaigns, user sign-ups for the fictitious social media start-up have again increased exponentially. The business wants to cash in on its success by adding premium accounts that are promoted to regular users. Premium accounts are a way for companies to gain followers on the social media website so they can profit from enhanced brand loyalty.
不幸的是,正如很多情况一样,基于 RPC 的集成扩展性不佳。它最初因其上市时间优势而被视为完美的选择,但现在却因为开发人员花费过多时间进行应急处理而导致功能无法交付。因此,在添加新功能之前,您需要先稳定系统,以支持业务的快速增长。
Unfortunately, as happens on many occasions, the RPC-based integration is not scaling well. It was the perfect choice initially for its time-to-market advantages but is now preventing features from being delivered because developers spend too long fire-fighting. So before you can add new features, you need to stabilize the system to support the rapid growth of the business.
新版系统基于事件驱动架构。有趣的是,该系统并未采用上一章介绍的通用消息总线方法,而是使用了 REST 和 HTTP,从而避免了系统生命周期内的技术/供应商锁定。
This new version of the system is based on an event-driven architecture. Interestingly, though, instead of taking the common message bus approach (as per the previous chapter), this system uses REST and HTTP to preclude technology/vendor lock-in for the lifetime of the system.
正如上一章所述,少量的设计可以创建共同的愿景,并加深对正在构建的系统如何满足其功能性和非功能性需求的理解。设计使用 REST 进行集成的系统的一些步骤与设计消息传递系统的步骤有所不同。但大多数步骤是相似的,包括第一步:从领域开始。
As suggested in the previous chapter, a small amount of design can create a shared vision and a deeper understanding of how the system being built addresses its functional and nonfunctional requirements. Some steps for designing a system that uses REST for integration will differ from those used to design a messaging system. Mostly the steps will be similar, though, including the first step: start with the domain.
使用 UL 在一组草图中表达业务策略,再次成为设计系统的重要第一步。在确定技术解决方案之前,弄清楚需要解决的问题以及需要建模的领域流程几乎总是有意义的。
Expressing business policies, using the UL, in a set of sketches is again a useful first step in designing a system. It nearly always makes sense to work out what problem you need to solve and what domain processes you need to model before you decide on a technical solution.
图 13.9是一个组件图,展示了“推荐账户”用例的全新事件驱动设计。与上一章中的消息传递解决方案一样,它专注于领域命令和事件。实际上,这种设计也可以用于消息传递系统,因为该图关注的是业务用例期间的消息流,并且与技术选择无关。
Figure 13.9 is a component diagram illustrating the new event-driven design of the Recommended Accounts use case. As with the messaging solution in the previous chapter, it focuses on domain commands and events. In fact, this design could be for a messaging system, because the diagram focuses on the flow of messages during the business use case and is independent of technology choices.
图 13.9推荐账户用例的组件图。
FIGURE 13.9 Component diagram for the Recommended Accounts use case.
在图 13.9中,您可以看到两个关键的领域事件。首先是“开始关注”。在整个公司,每位员工都知道,“开始关注”领域事件发生在一个帐户开始关注另一个帐户时。这是 UL 的一部分,也是公司的核心概念之一。显示的另一个领域事件是“已识别高级推荐”。这也是 UL 的一部分,表示发现限界上下文已识别高级帐户的事件普通账户可能愿意关注。这是一个至关重要的域名概念,因为向普通用户推广高级账户是新商业模式的核心。
In Figure 13.9, you can see two key domain events. First, there is Began Following. Throughout the company, every member of staff understands that a Began Following domain event occurs when one account starts following another. This is part of the UL and one of the core concepts of the business. The other domain event shown is Premium Recommendations Identified. This is also part of the UL, representing occurrences where the Discovery bounded context has identified a premium account that a regular account may like to follow. This is a crucial domain concept, because promoting premium accounts to regular users is central to the new business model.
第 11 章向您展示了遵循 SOA 原则可以实现松耦合的有界上下文。反过来,这可以为高效团队提供平台。SOA 的原则与技术无关,因此您可以在使用 HTTP 构建系统时应用它们。
Chapter 11 showed you that following the principles of SOA can lead to loosely coupled bounded contexts. In turn, this can provide the platform for high-performing teams. SOA’s principles are technology agnostic, so you can apply them when building systems with HTTP.
通过隔离每个团队拥有的有界上下文,松耦合变得触手可及。每个团队都可以根据其业务优先级自由开发功能,避免跨团队干扰或依赖。与消息传递相比,使用基于 HTTP 的 SOA 的唯一区别在于,团队之间的契约不再是代码中的类,而是 HTTP 请求和响应的格式。接下来的示例将充分展示这一点。
By isolating bounded contexts each owned by a single team, loose coupling is within reach. Each team is free to develop its features in line with its business priorities, free of cross-team distractions or dependencies. All that changes when using SOA with HTTP compared to messaging is that the contract between teams is no longer classes in code, but the format of HTTP requests and responses. Upcoming examples fully demonstrate this.
您在第 11 章和第 12 章中看到,基于响应式原则的异步消息传递是构建容错可扩展系统的推荐方法。与 REST 和 HTTP 集成时也是如此。您可能知道,HTTP 本身并不支持发布/订阅模式,因此,无法像消息总线那样在事件发生时将其推送给订阅者。相反,使用 REST,客户端可以轮询更改。轮询通常在扩展方面存在负面影响,但利用 HTTP 的缓存约定可以解决这个问题。
You saw in Chapters 11 and 12 that asynchronous messaging, based on reactive principles, was the recommendation for building fault-tolerant scalable systems. This is also the case when integrating with REST and HTTP. As you are probably aware, HTTP doesn’t inherently support publish/subscribe, so there’s no way to push out events to subscribers as they occur like you can with a message bus. Instead, with REST, clients can poll for changes. Polling generally has negative connotations in terms of scaling, but utilization of HTTP’s caching conventions can negate this problem.
图 13.10是本章剩余部分将构建的全新反应式 RESTful 社交媒体系统的容器图。请注意,它与消息传递系统有一些相似之处:每个组件都很小,因此可以根据业务需求独立扩展;有界上下文不共享数据库等依赖项。此外,由于 HTTP 的存在,每个团队都可以在其有界上下文中使用任何自己喜欢的技术(这在上一章中比较难实现)。这些特性支持可扩展性、容错性和开发速度。
Figure 13.10 is the containers diagram for the new Reactive, RESTful social media system that will be built in the remainder of this chapter. Note how there are some similarities to a messaging system: each component is small so that it can be scaled independently according to business needs; bounded contexts do not share dependencies such as databases. Additionally, thanks to HTTP, each team can use any technologies it prefers within its bounded contexts (which was more difficult to achieve in the previous chapter). These traits support scalability, fault tolerance, and development velocity.
图 13.10发现、帐户管理和营销有界上下文的容器图。
FIGURE 13.10 Containers diagram of Discovery, Account Management, and Marketing bounded contexts.
可扩展性的一个重要设计考虑因素是项目的粒度。对于 HTTP API,您可以将所有端点放在一个项目中,但这样一来,您就无法轻松地根据它们各自的可扩展性需求独立部署它们。本章中较为宽泛的建议是,一开始就为每个资源创建一个项目。图 13.10中的示例是入口点资源和帐户资源的独立项目。
An important design consideration for scalability is the granularity of projects. For your HTTP APIs, you could put all your endpoints in one project, but then you couldn’t easily deploy them independently based on their individual scalability needs. The loose recommendation in this chapter is to start with one project per resource. Examples of this in Figure 13.10 are the stand-alone projects for the entry point resource and the Accounts resource.
当您拥有嵌套资源时,有时您可能还需要考虑将它们移入其自己的项目。这样做的主要因素通常是,额外项目的复杂性与独立扩展 API 的能力之间存在矛盾。如果特定用例对可扩展性有很高的要求,您甚至可能希望将单个请求处理程序移入其自己的项目。
When you have nested resources, you may also want to consider moving them into their own project sometimes. The main factors for doing so will usually be complexity of having extra projects traded off against the ability to scale APIs independently. You may even want to move individual request handlers into their own project if specific use cases have demanding scalability requirements.
超媒体是 REST 的基础,因为它使得客户端和服务器能够彼此独立地发展。因此,在开始构建系统之前,预先考虑一下超媒体的契约会很有帮助。(你仍然可以在构建过程中不断迭代。)设计 REST 工作流的一个好方法是使用如图 13.11所示的序列图,该图演示了事件驱动的“添加关注者”用例。
Hypermedia is fundamental to REST because it is the factor that enables clients and servers to evolve independently of one another. So it can be useful to think a little about the hypermedia contracts up front before you start to build the system. (You can still iterate as you go along.) A good way to design REST workflows is to use sequence diagrams like the one shown in Figure 13.11 that demonstrates the event-driven Add Follower use case.
图 13.11添加关注者用例的 HTTP 请求流。
FIGURE 13.11 Flow of HTTP requests for the Add Follower use case.
如您所见,客户端仅与入口点 URI 耦合。从那里,它们通过响应中提供的超媒体链接向其他服务发出请求。例如,客户端想要在帐户管理中启动“添加关注者”用例(类似于域命令)。有界上下文仅与入口点资源的 URI( /accountmanagement)耦合。从那时起,客户端只需跟随超媒体中的链接(以 HTTP 响应的形式返回),直到到达关注者资源。
As you can see, clients are only coupled to the entry point URI. From there, they make requests to other services by following hypermedia links provided in responses. For instance, clients wanting to initiate the Add Follower use case (akin to a domain command) on the Account Management bounded context are only coupled to the URI of the entry point resource—/accountmanagement. From then on, clients merely follow links in the hypermedia, returned as HTTP responses, until the Followers resource is reached.
在图 13.10所示的容器图中,每个 HTTP 端点旁边都标明了其内容类型。大多数都显示为 application/hal+json。HAL 代表超文本应用程序语言;它本质上就是众所周知的现有内容类型(XML 和 JSON),并带有表示超媒体链接的约定。您可以在 Mike Kelly 的博客 ( http://stateless.co/hal_specification.html ) 上深入了解 HAL 标准。本章其余部分的示例使用 application/hal+json,因此您只需阅读本章即可学习基础知识。
In the containers diagram shown in Figure 13.10, there is an indication next to each HTTP endpoint of its content type. Most of them show application/hal+json. HAL stands for Hypertext Application Language; it is essentially just well-known existing content types—XML and JSON—with conventions for representing hypermedia links. You can learn about the HAL standard in depth on Mike Kelly’s blog (http://stateless.co/hal_specification.html). The examples in the remainder of this chapter use application/hal+json, so you will still learn the basics just by reading this chapter.
容器图上显示的另一种内容类型是 application/atom+xml,它表示 Atom。Atom 是生成 RSS 源的通用标准,因此非常适合表示事件列表。这正是帐户管理限界上下文使用它的方式——表示已发生的“开始关注”事件列表。
Another content type shown on the containers diagram is application/atom+xml, which denotes Atom. Atom is a common standard for producing RSS feeds and thus is a great fit for representing lists of events. This is precisely how it is used by the Account Management bounded context—to represent the list of Began Following events that have occurred.
本章将使用 Atom 作为事件源,这是构建基于 REST 的事件驱动分布式系统的主要基石。不一定非要使用 Atom,但 Atom 的流行度绝对值得考虑。
Using Atom as a feed of events is the main building block for building event-driven distributed systems with REST in this chapter. It doesn’t have to be Atom, but Atom’s popularity means you should definitely consider it.
图 13.12展示了一个轮询 Atom 提要以获取领域事件的示例。该图展示了轮询 Began Following Atom 提要所涉及的 HTTP 消息流,您将在本章后面构建该提要。
An example of polling an Atom feed for domain events is shown in Figure 13.12. This diagram shows the flow of HTTP messages involved for polling the Began Following Atom feed that you will build later in this chapter.
图 13.12轮询和使用“开始关注”事件源的 HTTP 请求流。
FIGURE 13.12 Flow of HTTP requests for polling and consuming the Began Following event feed.
现在您已经了解了所有理论知识,是时候开始构建 RESTful 风格的、事件驱动的社交媒体应用了。为了保持示例简洁并专注于基本模式,您将只构建账户管理有界上下文和部分发现有界上下文。通过这些示例,您将学到足够的知识,可以在您自己的项目中运用这些概念。
Now that you’ve heard all the theory, it’s time to start building the RESTful, event-driven version of the social media application. To keep the examples concise and focused on essential patterns, you will just be building the Account Management bounded context and some of the Discovery bounded context. From these examples, you will learn enough to begin using the concepts on your own projects.
我们将采用由外而内的方式构建 RESTful 社交媒体系统;首先,您将向“帐户管理”限界上下文添加超媒体入口点。然后,您将创建帐户 API。之后,您将创建用于发布“开始关注”事件的 Atom 源。最后,您将创建位于“发现”限界上下文中的“开始关注”源的消费者。
An outside-in approach will be taken to build the RESTful social media system; you will start by adding the hypermedia entry point to the Account Management bounded context. You’ll then create the Accounts API. After that you’ll create the Atom feed that publishes Began Following events. Finally, you’ll create the consumer of the Began Following feed that resides in the Discovery bounded context.
在接下来的示例中,为了方便起见,所有代码都将位于同一个 Visual Studio 解决方案中。但是,当您构建通过 HTTP 集成的 RESTful 系统时,这并非必要。事实上,您可能正在使用不同的技术,甚至无法共享解决方案。您可以自行决定哪种方案最佳;为系统的每个部分设置完全独立的代码存储库可以降低耦合度,但将代码紧密地放在一起可能有助于共享全局。
During the upcoming examples, all code will live in the same Visual Studio solution for convenience. But when you’re building RESTful systems that integrate over HTTP, this is not necessary. In fact, you may be using different technologies where it is not even possible to share a solution. It’s up to you to decide what you think is best; having completely separate code repositories for each part of the system encourages looser coupling, but keeping code close together may help to share the bigger picture.
要开始构建新的 RESTful 社交媒体系统,您可以先创建一个名为PPPDDD.REST.SocialMedia的新空白 Visual Studio 解决方案。
To begin building the new RESTful social media system, you can start by creating a new blank Visual Studio solution called PPPDDD.REST.SocialMedia.
超媒体是 REST 的核心。因此,您现在将了解如何使用 ASP.NET Web API 在 .NET 中构建超媒体 API。虽然实现细节因框架而异,但这些概念与框架无关,并且适用于您用于构建超媒体 API 的任何工具。
Hypermedia is at the heart of REST. Accordingly, you will now see how to build hypermedia APIs in .NET with ASP.NET Web API. Although the implementation details will vary from framework to framework, the concepts are framework agnostic and will apply to any tools you may decide to build hypermedia APIs with.
正如你所见,超媒体的主要优势在于它解耦了客户端和服务器,允许它们独立发展。但客户端必须预先了解 API 的一些信息。这就是客户端应该将自身与入口点资源进行耦合的作用。
As you’ve seen, the key benefit of hypermedia is that it decouples clients and servers, allowing independent evolution. But clients must know something up front about the API. This is the role of the entry point resource that clients should couple themselves to.
当客户端想要与 REST API 交互时,他们首先会请求入口点资源。此后,他们通常只需跟随返回的超媒体中的链接即可。选择入口点的位置需要考虑许多因素。例如,图 13.10中的设计选择每个有界上下文只有一个入口点。但是你可以为整个系统设置一个入口点,或者更细粒度地为每个顶级资源设置一个入口点。最终,你必须根据每个项目的具体情况决定希望通过入口点资源公开多少系统内容。
When clients want to interact with a REST API, they start by requesting the entry point resource. From then on, they mostly just follow links in the hypermedia that is returned. Choosing where to locate your entry point(s) has a number of considerations that you should take into account. For instance, the design in Figure 13.10 chooses to have a single entry point per-bounded context. But you could have a single entry point for the entire system or go more fine-grained and have an entry point per top-level resource. Ultimately, you have to decide on a per-project basis how much of the system you want to expose via entry point resources.
设计入口点涉及确定 API 使用者应使用的初始资源和转换。帐户管理限界上下文的入口点将是顶级资源列表。在本例中,这只是帐户资源。从帐户资源开始,超媒体将链接到各个帐户,并从各个帐户链接到这些帐户的详细信息,这些详细信息以子资源的形式公开,例如其关注者。这只是重复您在本章前面的序列图中看到的内容。
Designing an entry point involves identifying the initial resources and transitions that should be available to consumers of the API. The Account Management bounded context’s entry point will be the list of top-level resources. In this example, that is just the Accounts resource. From the Accounts resource, hypermedia will link to individual accounts, and from individual accounts to details of those accounts, exposed as child resources, such as its followers. This is just repeating what you saw in the sequence diagram earlier in the chapter.
正如您开始看到的,链接是超媒体的基石。但是,为了让 API 客户端能够跟踪链接,他们需要能够决定哪个链接提供了他们想要的过渡。这就是链接关系的作用,它表明了链接所代表的内容(或者更具体地说,它与当前资源的关系)。例如,对于包含多个页面的资源,要符合超媒体规范,它需要一个带有“Next”关系的链接,客户端可以通过该链接跳转到下一页。在接下来的示例中,您将看到许多链接和关系。
As you’re starting to see, links are the building blocks of hypermedia. But for API clients to follow links, they need to be able to decide which link provides the transition they are looking for. This is the role of link relations, which indicate what the link represents (or, more specifically, its relationship to the current resource). For example, for a resource that has many pages, to be hypermedia-compliant, it would need a link with the relation Next, which clients can follow to the next page. You’ll see a number of links and relations in the upcoming examples.
要构建生成帐户管理入口点的 API,您需要首先向解决方案中添加一个名为AccountManagement.EntryPoint的新 ASP.NET 应用程序。这遵循基于格式{bounded context}.{Resource}命名 API 项目的约定。将项目添加到解决方案时,请务必选择空模板,并确保选中“Web API”复选框。
To build the API that produces the Account Management entry point, you need to start by adding a new ASP.NET application to the solution called AccountManagement.EntryPoint. This follows the convention of naming API projects based on the format {bounded context}.{Resource}. When adding the project to your solution, be sure to select the empty template, and be sure to check the Web API check box.
在添加生成入口点资源的端点之前,仍需要做出一个决定。您需要选择一种支持超媒体的媒体类型。
One decision still needs to be made before you can add the endpoint that produce the entry point resource. You need to choose a media type that supports hypermedia.
传统上,XHTML 一直被用作 REST API 的超媒体格式。不幸的是,由于其冗长,它并不受欢迎(尤其是在试图摆脱 SOAP 的情况下)。幸运的是,不过,现在有一个相对较新的标准正在逐渐普及。这个新标准就是 HAL,本章前面已经简要介绍过,它主要有两种类型:XML 和 JSON。本质上,这两种众所周知的格式都通过特定的链接表示约定进行了扩展。这既提供了 XHTML 的超媒体优势,又避免了其冗长,如下例所示。
Traditionally, XHTML has been used as the hypermedia format for REST APIs. Unfortunately, it’s undesirable due to its verbosity (especially if you’re trying to escape from SOAP). Fortunately, though, a relatively new standard is available and gathering traction. This new standard is HAL, which was briefly introduced earlier in the chapter, and it comes in two main flavors: XML and JSON. Essentially, both of those well-known formats have been extended with specific conventions for representing links. This provides the hypermedia benefits of XHTML without the verbosity as the following example demonstrates.
{
"_links": {
"self": {
"href":"http://localhost:4100/accountmanagement"
},
"accounts": {
"href":"http://localhost:4101/accounts"
},
}
}
{
"_links": {
"self": {
"href":"http://localhost:4100/accountmanagement"
},
"accounts": {
"href":"http://localhost:4101/accounts"
},
}
}
上述代码片段中显示的入口点资源演示了在 HAL (JSON) 中表示链接的约定。所有链接都必须在资源根目录下名为_links的元素内定义。每个链接都以其关系(示例中为self和accounts)开头。每个链接还包含一个 href,即其指向的资源的 URI。只有指向当前资源的self链接是必需的;所有其他链接都是可选的。
The entry point resource shown in the preceding snippet demonstrates the conventions for representing links in HAL (JSON). All links must be defined within an element at the root of the resource called _links. Each link begins with its relation (self and accounts in the example). Each link also contains an href, which is the URI of the resource it points to. Only the self link, which points to the current resource, is mandatory; all other links are optional.
要构建入口点资源 API,首先需要将项目配置为在端口 4100 上启动。(可以在项目属性的【Web】选项卡上进行设置。)然后,需要通过在 Web API 的 WebAPIConfig 中添加路由来配置入口点的 URI ,如清单 13-8 所示。WebApiConfig文件将位于项目根目录中的App_Start文件夹中。
To build the entry point resource API, you first need to configure the project to start on port 4100. (You can set this in the project’s properties on the Web tab.) You then need to configure a URI for the entry point by adding a route inside Web API’s WebAPIConfig, as shown in Listing 13-8. Your WebApiConfig file will be located in the App_Start folder that sits in the root of the project.
如果您不熟悉 Web API 的路由语法,在清单 13-8 中,入口点路由的定义确保每当路径 /accountmanagement 收到请求时,都会调用名为EntryPointController的类上的Get()方法。在实现该控制器之前,您需要安装一个 Nuget 包,该包在 Web API 中添加了对 HAL 的支持。正如您将看到的,这是一个非常实用的库,它省去了创建 HAL API 的所有精力。要将WebApi.Hal安装到AccountManagement.EntryPoint.Api项目中,您需要在 Nuget 包管理器控制台中运行以下命令:
If you’re not familiar with Web API’s routing syntax, in Listing 13-8, the definition of the Entry Point route ensures that whenever a request comes in for the path /accountmanagement, the Get() method on a class called EntryPointController is invoked. Before you can implement that controller, you need to install a Nuget package that adds support for HAL in Web API. As you will see, this is an incredibly usable library that takes all the effort out of creating HAL APIs. To install WebApi.Hal into the AccountManagement.EntryPoint.Api project, you need to run the following command in the Nuget Package Manager Console:
Install-Package WebApi.Hal -Project AccountManagement.EntryPoint.Api
Install-Package WebApi.Hal -Project AccountManagement.EntryPoint.Api
安装WebApi.Hal后,您需要通过更新Global.asax.cs文件来告诉 Web API 将 HAL (JSON) 设置为默认媒体类型,如清单 13-9 所示。这也会将 HAL (XML) 设置为第二首选项。这两个格式化程序JsonHalMediaTypeFormatter和XmlHalMediaTypeFormatter都属于您刚刚安装的WebAPI.Hal包。
Once WebApi.Hal is installed, you need to tell Web API to make HAL (JSON) the default media type by updating your Global.asax.cs, as per Listing 13-9. This also sets HAL (XML) as the second preference. The two formatters, JsonHalMediaTypeFormatter and XmlHalMediaTypeFormatter, belong to the WebAPI.Hal package you just installed.
所有依赖项均已安装完毕,配置也已完成,接下来可以通过实现EntryPointController来完成 Entry Point API 。您可以先通过以下方式实现:在项目根目录下的Controllers文件夹中添加一个名为EntryPointController的类。添加该类后,可以用清单 13-10 中的代码替换文件的内容。
All dependencies are installed and configuration is now complete, paving the way for you to complete the Entry Point API by implementing the EntryPointController. You can achieve this by first adding a class called EntryPointController to the Controllers folder that sits at the root of the project. Once you’ve added the class, you can then replace the contents of the file with the code in Listing 13-10.
清单 13-10 中的代码执行时,将默认以 HAL-JSON 格式返回入口点资源。这是因为EntryPointRepresentation继承自Representation基类——WebApi.Hal库提供的一个类。当从控制器方法返回继承自Representation 的类时, JsonHalMediaTypeFormatter会将其转换为 HAL-JSON。
The code in Listing 13-10, when executed, will return the entry point resource as HAL-JSON (by default). This is because EntryPointRepresentation inherits from the Representation base class—a class that the WebApi.Hal library provides. When a class inheriting from Representation is returned from a controller method, the JsonHalMediaTypeFormatter will convert it to HAL-JSON.
在Get()内部,代码以声明方式映射到响应格式。您可以看到此代码生成了两个链接:Href和Links(一个只有一个链接的集合)。这两个链接将出现在响应中。您可以通过测试目前为止的内容来了解所有这些操作的实际效果。
Inside Get(), the code declaratively maps onto the response format. You can see two links are being generated in this code: Href and Links (a collection with just one link). These are the two links that will appear in the response. You can see all this in action by testing out what you have so far.
基于通用标准构建超媒体 API 的一大优势在于,可以轻松创建通用客户端,用于探索使用这些标准构建的 API。HAL 的其中一种工具就是 Hal 浏览器,它是一个小型 Web 应用,允许您使用 HAL API 并与之交互。现在,您将了解如何使用 HAL 浏览器来测试您刚刚构建的入口点 API。
A huge benefit of building hypermedia APIs on top of common standards is that it is easy to create generic clients that can explore APIs built using those standards. One such tool for HAL is the Hal browser, a small web application that allows you to consume and interact with HAL APIs. You’ll now see how to use the HAL browser to test the Entry Point API you have just built.
要安装 HAL 浏览器,您需要下载它(https://github.com/mikekelly/hal-browser/archive/master.zip)并解压。最后,您需要将解压后的文件夹内容复制到Windows 资源管理器中AccountManagement.EntryPoint.Api项目的根目录下。如果您在 Windows 资源管理器中打开AccountManagement.EntryPoint.Api项目的根目录,应该会看到一个名为browser.html的文件。如果您没有看到该文件,则说明该文件的位置不正确。
To install the HAL browser, you have to download it (https://github.com/mikekelly/hal-browser/archive/master.zip) and then unzip it. Finally, you need to copy the contents of the unzipped folder into the root of your AccountManagement.EntryPoint.Api project’s folder in Windows Explorer. If you open Windows Explorer at the root of the AccountManagement.EntryPoint.Api project, there should be a file called browser.html. If you don’t see it, the file is in the wrong location.
如果您确认文件已复制到正确的位置,则可以按 F5 运行项目。在 Visual Studio 启动的浏览器中,您可以通过导航至http://localhost:4100/browser.html来访问 HAL 浏览器。(这假设您已将此项目配置为始终使用端口 4100,如前所述。)导航至browser.html后,如果一切正常,您将看到 HAL 浏览器,如图13.13所示。
If you are happy that the files are copied into the correct location, you can press F5 to run your project. In the browser that Visual Studio starts up for you, you can access the HAL browser by navigating to http://localhost:4100/browser.html. (This assumes that you configured this project to always use port 4100, as explained previously.) After navigating to browser.html, if everything is working correctly, you will see the HAL browser, as per Figure 13.13.
图 13.13访问 HAL 浏览器。
FIGURE 13.13 Accessing the HAL browser.
当您访问 HAL 浏览器时,您会注意到它没有显示您的入口点资源。这是因为 HAL 浏览器在默认路径 (/) 下查找 API 入口点。要解决这个问题,只需在 HAL 浏览器的导航栏(Explorer 标签下方)输入/accountmanagement,然后点击“GO”。然后,您应该会看到原始入口点资源(在右侧)和交互式工具(在左侧),如图13.14所示。
When you do access the HAL browser, you’ll notice it doesn’t show your entry point resource. This is because the HAL browser looks for API entry points at the default path (/). To remedy this, simply enter /accountmanagement into the HAL browser’s navigation bar (below the Explorer label) and click GO. You should then see the raw entry point resource (on the right side) and the interactive tools (on the left side), as shown in Figure 13.14.
图 13.14在 HAL 浏览器中查看入口点资源。
FIGURE 13.14 Viewing the entry point resource in the HAL browser.
目前,HAL 浏览器几乎没有提供什么帮助,因为入口点资源中的链接指向了不存在的资源。所以你的下一个任务是:实现 Accounts API。如图13.14中的入口点资源所示,Accounts 资源需要能够通过http://localhost:4101/accountmanagement/accounts访问。
At the moment, the HAL browser provides little benefit because the links in the entry point resource point to nonexistent resources. So this is your next task: to implement the Accounts API. As the entry point resource in Figure 13.14 shows, the Accounts resource needs to be accessible at http://localhost:4101/accountmanagement/accounts.
在构建 Accounts API 的过程中,你将了解如何解决人们对超媒体的一个常见问题:导航效率低下。假设一个 API 暴露了大量数据,例如数千甚至数百万个账户。客户端可能需要浏览数百个链接才能找到所需的资源,成功跟踪到下一页的链接。显然,这对于客户端和服务器来说效率非常低下,尤其是对于拥有大量并发客户端的公共 API 来说。这个问题可以通过使用 URI 模板来解决。
In building the Accounts API, you will see how to handle a common concern people have for hypermedia: inefficient navigation. Consider an API that exposes lots of data, such as thousands or millions of accounts. Clients may have to navigate hundreds of links to find the resource they want by successfully following links to the next page. Obviously, this can be massively inefficient for the client and the server—especially for public APIs with many concurrent clients. This problem is solved by using URI templates.
以下示例展示了 Accounts 资源的 HAL (JSON) 格式,您稍后将为其创建 API。查找 URI 模板;它是templated属性设置为true 的链接:
The following sample shows the Accounts resource, as HAL (JSON), that you are going to create an API for shortly. Look for the URI template; it’s the link whose templated attribute is set to true:
{
"_links": {
"self": {
"href":"http://localhost:4101/accountmanagement/accounts"
},
"alternative": {
"href":"http://localhost:4101/accountmanagement/accounts?page=1"
},
"account": [
{
"href":"http://localhost:4101/accountmanagement/accounts/{accountId}",
"templated": true
},
{
"href":"http://localhost:4101/accountmanagement/accounts/123"
},
...
}
{
"_links": {
"self": {
"href":"http://localhost:4101/accountmanagement/accounts"
},
"alternative": {
"href":"http://localhost:4101/accountmanagement/accounts?page=1"
},
"account": [
{
"href":"http://localhost:4101/accountmanagement/accounts/{accountId}",
"templated": true
},
{
"href":"http://localhost:4101/accountmanagement/accounts/123"
},
...
}
要表示 URI 模板,您不仅需要将templated属性设置为true,还必须在 URI 中添加占位符部分。在刚刚显示的 Accounts 资源响应中,您可以看到占位符是{accountId}。然后,API 的客户端可以将占位符替换为他们要查找的帐户的 ID。这样,您就可以将数百个潜在请求精简为一个。使用WebApi.Hal创建 URI 模板几乎不费吹灰之力,正如您在创建 Accounts API 时所看到的那样。
To represent URI templates, not only do you need to set the templated attribute to true, but you must also add placeholder sections in the URI. In the Accounts resource response just shown, you can see the placeholder is {accountId}. Clients of the API can then replace the placeholder with the ID of the account they are looking for. In doing so, you cut down hundreds of potential requests into just a single one. Creating URI templates with WebApi.Hal requires little effort, as you will now see while creating the Accounts API.
您可以通过添加一个名为AccountManagement.Accounts.Api的新项目来创建 Accounts API 。您需要将其设置为启动项目,并确保它在端口 4101 上运行。创建完成后,您可以通过在 Nuget 包管理器控制台中运行以下命令来添加对WebApi.Hal的引用:
You can create the Accounts API by adding a new project called AccountManagement.Accounts.Api. You need to set this as a start-up project and ensure that it runs on port 4101. Once it’s created, you can then add a reference to WebApi.Hal by running the following command in the Nuget Package Manager console:
Install-Package WebApi.Hal -Project AccountManagement.Accounts.Api
Install-Package WebApi.Hal -Project AccountManagement.Accounts.Api
最后的配置步骤是将 HAL 设置为默认内容类型,如前面的清单 13-9 所示。
The final configuration step is to set HAL as the default content type, as shown previously in Listing 13-9.
Accounts API 将公开两个 URI。首先,客户端将单击 /accountmanagement/accounts 上的 Accounts 资源,该资源代表整个帐户列表。然后,客户端将使用 Accounts 资源中包含的 URI 模板(accountmanagement/accounts/{accountId})导航到各个帐户。要在项目中声明这些路由,AccountManagement.Accounts.Api项目中的WebApiConfig应类似于清单 13-11。
Two URIs will be exposed by the Accounts API. Initially, clients will click the Accounts resource at /accountmanagement/accounts, which represents the entire list of accounts. From there, clients will then navigate to individual accounts using the URI template contained within the Accounts resource—accountmanagement/accounts/{accountId}. To declare these routes in your project, the WebApiConfig in your AccountManagement.Accounts.Api project should resemble Listing 13-11.
如清单 13-11 中的路由声明所示,需要将一个名为AccountsController的类添加到Controllers文件夹(仍在AccountManagement.Accounts.Api内)。它需要两个方法—— Index()和Accounts() ——由路由定义决定。从Index()开始,AccountsController最初应该包含清单 13-12 所示的代码。
As the route declarations in Listing 13-11 show, a class called AccountsController needs to be added to the Controllers folder (still inside AccountManagement.Accounts.Api). It needs two methods—Index() and Accounts()—as dictated by the route definitions. Starting with just Index(), the AccountsController should initially contain the code shown in Listing 13-12.
清单 13-12 中的大部分代码与清单 13-10 中的代码类似,但值得注意的是新增的链接。备用链接关系表示具有不同 URI 但指向同一资源(self)的链接。这样做的好处之一是,客户端可以通过将两个 URI 视为同一资源来更高效地缓存。备用链接下方是模板化的帐户链接,由于href包含占位符,WebApi.Hal会自动将其标记为模板化。
Most of the code in Listing 13-12 will be familiar from Listing 13-10, but it is worthwhile noting the additional links. The alternative link relation represents links that have a different URI but point to the same resource (self). One benefit of this is that clients can cache more efficiently by treating both URIs as the same resource. Below the alternative link is the templated account link, which WebApi.Hal automatically marks as templated because the href contains a placeholder.
在 HAL 浏览器中测试新 API 之前,您需要启用跨域资源共享 (CORS)。这是因为 Accounts 资源由不同的虚拟主机(端口 4101 而不是 4100)提供。启用 CORS 的详细说明请参阅 ASP.NET 网站 ( http://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api )。基本上,您需要执行以下操作:
Before you can test the new API in the HAL browser, you need to enable Cross-Origin Resource Sharing (CORS). This is because the Accounts resource is provided by a different vhost (port 4101 as opposed to 4100). The instructions for enabling CORS are detailed on the ASP.NET website (http://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api). Basically, you need to do the following:
通过在 Nuget 包管理器控制台中运行以下命令将 CORS 包添加到项目中:
Install-Package Microsoft.AspNet.WebApi.Cors -Project AccountManagement.Accounts.ApiAdd the CORS package to the project by running the following command inside the Nuget Package Manager Console:
Install-Package Microsoft.AspNet.WebApi.Cors -Project AccountManagement.Accounts.Api如果您像以前一样运行 HAL 浏览器,从入口点开始并按照指向 Accounts 资源的链接,您将看到 URI 模板链接,如图13.15所示。
If you run the HAL browser as before, starting at the entry point and following the link to the Accounts resource, you will see the URI template link, as per Figure 13.15.
图 13.15在 HAL 浏览器中跟踪帐户链接。
FIGURE 13.15 Following the Accounts link in the HAL browser.
URI 模板并非全有或全无;您可以将它们与普通链接组合使用,即使对于同一资源也是如此。图 13.15包含两个非模板化的“帐户”链接来说明这一点。它们直接指向 Account 资源。但是,要使这些链接正常工作,您需要向AccountsController添加一个Account()方法。(这由清单 13-11 所示的路由条目决定。)
URI templates are not all or nothing; you can combine them with normal links, even for the same resource. Figure 13.15 contains two nontemplated “account” links that illustrate this. They point directly to Account resources. For those links to work, though, you need to add an Account() method to the AccountsController. (This is determined by the route entry shown in Listing 13-11.)
Account()的初始实现(仅返回预设数据)如清单 13-14 所示。您可以将其添加到AccountsController 的Index()下方。您还需要添加清单 13-15 中的AccountRepresentation和Account类。(您可以将它们全部放在AccountsController.cs文件中。)
An initial implementation of Account(), which returns just canned data, is shown in Listing 13-14. You can add this to your AccountsController below Index(). You also need to add the AccountRepresentation and Account classes from Listing 13-15. (You can put them all inside the AccountsController.cs file.)
资源的字段(而不是链接)在 HTTP 响应中以标准 JSON 格式表示。在清单 13-15 中,您可以看到AccountRepresentation具有属性AcountId和名称。因此,如果您使用 Hal 浏览器导航到 Account 资源,您将看到这些属性以纯 JSON 格式表示。图 13.16也展示了这一点。
A resource’s fields (as opposed to its links) are represented as standard JSON in an HTTP response. In Listing 13-15, you can see that an AccountRepresentation has the properties AcountId and Name. Therefore, if you navigate to an Account resource using the Hal browser, you will see these properties represented as plain JSON. Figure 13.16 also shows this.
图 13.16资源的数据字段以纯 JSON 表示。
FIGURE 13.16 Resource’s data fields are represented as plain JSON.
图 13.16还显示了一些值得注意的链接。具体来说,指向此帐户子资源的三个链接分别是followers、following和blurbs。您将在下一节中构建 followers 端点,但其他两个链接只是为了让您了解具有许多子资源的资源应该是什么样子。构建followers端点之后,您将开始使用事件存储构建系统的事件驱动部分。
Figure 13.16 also shows some noteworthy links. In particular, the three links that point to child resources of this account are followers, following, and blurbs. You’re going to build the followers endpoint in the next section, but the other two links are just to give you an idea of how a resource with many child resources could look. Building the followers endpoint is where you will start to build the event-driven parts of the system using the Event Store.
大部分基础架构都已准备就绪,可供您学习如何使用 REST 构建事件驱动系统。学习的第一部分涉及存储事件。您可以通过多种方式实现这一点,例如将事件写入文本文件日志或使用 SQL 数据库中的表。但在本例中,您将看到一个专门构建的工具——事件存储 (Event Store),它是由著名的 DDD 从业者 Greg Young 及其团队 ( www.geteventstore.com ) 开发的。
Most of the infrastructure is in place for you to learn about building event-driven systems with REST. The first part of your learning involves storing events. There are many ways you can achieve this, such as writing events to a text file log or using a table in a SQL database. But in this example, you will see a purpose-built tool—the Event Store—that is the work of popular DDD practitioner Greg Young and his team (www.geteventstore.com).
要了解如何存储事件,您需要在 Accounts API 中构建一个新的端点,该端点返回帐户的关注者。此端点支持通过发布到集合中来添加新的关注者。此流程之前已在图 13.11中说明。像往常一样,创建用于公开资源的新端点的第一步是从路由定义开始。清单 13-16 显示了如何更新WebApiConfig以添加 Followers 端点的路由定义。
To learn about storing events, you need to build a new endpoint in your Accounts API that returns the followers of an account. This endpoint supports the ability to add new followers to the collection by posting to it. This flow was previously illustrated in Figure 13.11. As usual, the first step to creating a new endpoint for exposing resources is to start with the route definition. Listing 13-16 shows how your WebApiConfig should be updated to add the route definition for the Followers endpoint.
清单 13-16 显示,Followers 资源将由名为FollowersController 的控制器上的Index()方法提供。您可以通过在项目的Controllers文件夹中添加一个名为FollowersController的类来创建该控制器,类似于清单 13-17。
Listing 13-16 shows that the Followers resource will be served up by a method called Index() on a controller called FollowersController. You can create that controller by adding a class called FollowersController, in your project’s Controllers folder, that resembles Listing 13-17.
清单 13-17 中Index()的实现仅返回一个预设响应,并以传入的 Account ID 作为参数。这并不是本例的重点——响应发布到端点的数据并持久化事件才是重要且令人兴奋的部分。清单 13-18 中可以看到触发该过程,其中展示了需要添加到FollowersController中GetFollowers()正下方的代码。此外,还需要添加清单 13-19 中所示的BeganFollowing类。
The implementation of Index() in Listing 13-17 merely returns a canned response, parameterised with the passed-in Account ID. This is not the important part of this example—persisting an event in response to data being posted to the endpoint is the important and exciting part. You can see that process being triggered in Listing 13-18, which shows the code that needs to be added to your FollowersController directly below GetFollowers(). Also, you need to add the BeganFollowing class that is shown in Listing 13-19.
如清单 13-18 所示, IndexPOST()响应/accountmanagement/accounts/{accountId}/followers的 post 请求。您可以看到这一点,因为该方法具有HttpPost属性。由于同一文件中还有另一个名为Index () 的方法,因此ActionName属性指示即使此方法名为IndexPOST(),它仍应响应Account Followers路由的请求。
IndexPOST(), shown in Listing 13-18, responds to post requests for /accountmanagement/accounts/{accountId}/followers. You can see this because the method is attributed with the HttpPost attribute. Because of another method called Index () in the same file, the ActionName attribute indicates that it should still respond to requests for the Account Followers route even though this method is named IndexPOST().
关键的事件持久化机制并未包含在清单 13-18 中。不过,您可以看到对EventPersister.PersistEvent()的调用。这是处理事件持久化的类。您可以在清单 13-20 中看到它的内容,其中展示了将事件持久化到事件存储的最低限度的功能。您需要将此类添加到项目中。为了方便起见,您可以将其放在AccountsController.cs文件的底部(但在AccountsController类之外)。不过,在添加EventPersister之前,您需要使用以下命令安装事件存储 C# 客户端:
The crucial event-persistence mechanics are not included in Listing 13-18. You can, though, see a call to EventPersister.PersistEvent(). This is the class that handles event persistence. You can see the contents of it in Listing 13-20, which shows the bare-minimum functionality for persisting events to the Event Store. You need to add this class to your project. For convenience, you can put it at the bottom of the AccountsController.cs file (but outside of the AccountsController class). Before adding the EventPersister, though, you need to install the Event Store C# client with the following command:
Install-Package EventStore.Client -Project AccountManagement.Accounts.Api
Install-Package EventStore.Client -Project AccountManagement.Accounts.Api
为了编译EventPersister ,您需要包含以下使用语句:
For the EventPersister to compile, you need to include the following using statements:
using EventStore.ClientAPI;
using Newtonsoft.Json;
using System.Net;
using System.Text;
using EventStore.ClientAPI;
using Newtonsoft.Json;
using System.Net;
using System.Text;
在清单 13-20 所示的代码中,有两个关键细节需要关注:事件被转换为 JSON(然后转换为二进制),并被附加到一个流——在本例中是BeganFollowing流。一旦事件存储启动并运行,您将更好地理解所有这些是如何工作的。
Of the code shown in Listing 13-20, there are two key details to focus on: the event is converted to JSON (and then binary), and it is appended to a stream—the BeganFollowing stream in this case. You will get a better understanding of how all of this works once the Event Store is up and running.
本示例使用 Event Store 2.0.1 版本(http://download.geteventstore.com/binaries/EventStore-OSS-Win-v2.0.1.zip)。下载完成后,只需将压缩包解压到一个目录中,然后以管理员身份在该目录中运行以下 PowerShell 命令即可:
This example uses version 2.0.1 of the Event Store (http://download.geteventstore.com/binaries/EventStore-OSS-Win-v2.0.1.zip). Once you’ve downloaded it, you just need to extract the archive into a directory and then run the following PowerShell command from that directory (as Administrator):
./EventStore.SingleNode.exe --db .\ESData
./EventStore.SingleNode.exe --db .\ESData
要确认事件存储已成功启动,您应该能够通过http://localhost:2113访问管理应用程序。成功确认后,您将看到如图 13.17所示的欢迎页面。如果您没有看到欢迎屏幕,请仔细检查您是否以管理员身份运行了 PowerShell。此外,请查看控制台上是否打印了任何错误信息。PowerShell 控制台。如果您看到欢迎页面,则表示事件存储正在运行,并且正在耐心等待保存您的所有事件。
To confirm that the Event Store has started up successfully, you should be able to access the management application by going to http://localhost:2113. As confirmation of success, you are presented with the welcome page shown in Figure 13.17. If you don’t see the welcome screen, double-check that you ran PowerShell as Administrator. Also, look to see if there were any errors printed on the PowerShell console. If you do see the welcome page, that means the Event Store is running and is now patiently waiting to persist all your events.
图 13.17事件存储的管理 UI。
FIGURE 13.17 The Event Store’s admin UI.
您刚刚浏览了事件存储管理界面,现在您将通过查看通过 Accounts API 创建的事件来探索它的一些关键功能。要将事件导入系统,您需要发布新关注者的详细信息。为了演示,您可以在启动系统后,通过 HAL 浏览器转到入口点(HAL 浏览器中的 /accountmanagement)来完成所有操作。然后,您需要点击链接访问 Accounts 资源。从 Accounts 资源,您需要点击链接访问其中一个虚拟帐户,然后从那里点击链接访问其关注者资源。
You just saw a glimpse of the Event Store admin UI, and now you will explore some of its key features as you view events that are being created via the Accounts API. To get events into the system, you need to post details of new followers. For demonstration purposes, you can do all this through the HAL browser by going to the entry point (/accountmanagement in the Hal browser) after you have started the system. You then need to follow the link to the Accounts resource. From the Accounts resource, you need to follow the link to one of the dummy accounts, and from there follow the link to its followers resource.
如果要向关注者资源发布帖子,则需要点击代表自身链接的行的“NON-GET”列中的橙色按钮。此按钮如图 13.18所示。点击此按钮将打开一个对话框,允许您构建将发布到端点的 JSON 负载。图 13.19显示了正确格式的 JSON 数据正在被输入到此对话框中,其中包含新关注者的详细信息。输入 JSON 数据后,单击“确定”按钮,您的 JSON 数据便会发布。
If you want to post to the followers resource, you need to click the orange button in the NON-GET column for the row that represents the self link. This button is shown in Figure 13.18. Clicking this button opens a dialog enabling you to construct a JSON payload that will be posted to the endpoint. Figure 13.19 shows correctly formatted JSON being entered into this dialog containing the details of a new follower. Once you’ve entered some JSON, click the OK button, and your JSON is posted.
图 13.18 HAL 浏览器中的 NON-GET 按钮。
FIGURE 13.18 The NON-GET button in the HAL browser.
图 13.19在 HAL 浏览器中的 NON-GET 对话框中构建 JSON。
FIGURE 13.19 Constructing JSON on the NON-GET dialog in the HAL browser.
如果您在发布 JSON 后收到了200响应,现在就可以使用事件存储的管理界面查看该事件了。导航到http://localhost:2113/并选择“Streams”菜单项后,您应该会看到一个名为BeganFollowing的流,它是在您从 HAL 浏览器发布消息时创建的。您可以点击其名称来查看该流中的事件。然后,您可以检查各个事件,如图13.20所示。
Providing you got a 200 response back from posting the JSON, you can now view the event using the Event Store’s admin UI. After navigating to http://localhost:2113/ and choosing the Streams menu item, you should see a stream called BeganFollowing that was created when you posted from the HAL browser. You can click on its name to view events in that stream. From there, you can inspect individual events, as shown in Figure 13.20.
图 13.20查看事件存储中的事件。
FIGURE 13.20 Viewing an event in the Event Store.
Atom 是许多 RESTful 系统中事件公开的明智之选,因为它是一种极其常见的格式,正如本章前面所讨论的。在本例中,您将了解如何使用 .NET 框架内置的工具来创建和发布 Atom 源。
Atom is a discerning choice for exposing events in many RESTful systems because it is an extremely common format, as discussed earlier in the chapter. In this example, you will see how to use the tools baked into the .NET framework for creating and publishing an Atom feed.
将事件发布为 Atom 源的应用程序类似于消息传递系统中的消息发布组件。因此,对于事件驱动的 REST 系统,您可以使用类似的命名约定来传达领域概念,例如{BoundedContext}.{BusinessComponent}.{Component}。
Applications that publish events as an Atom feed are akin to message-publishing components in a messaging system. Accordingly, for an event-driven REST system, you can use a similar naming convention that communicates domain concepts, such as {BoundedContext}.{BusinessComponent}.{Component}.
要创建发布“Began Following”域事件的组件,可以先向项目中添加一个名为AccountManagement.RegularAccounts.BeganFollowing的新 ASP.NET Web 应用程序。您需要将此应用程序配置为在端口 4102 上运行,并使其成为启动项目。
To create the component that publishes the Began Following domain event, you can start by adding a new ASP.NET Web Application to the project called AccountManagement.RegularAccounts.BeganFollowing. You need to configure this application to run on port 4102 and make it a start-up project.
要使用 .NET Framework 的官方库创建 Atom 提要,首先需要在新的 AccountManagement.RegularAccounts.BeganFollowing 项目中添加对 System.ServiceModel 的引用。设置路由定义后,如清单 13-21 所示,然后可以使用 System.ServiceModel 中的类,使用从事件存储中检索到的事件创建 Atom 提要,如清单 13-22 所示。清单 13-22 中的代码需要作为新的控制器添加到Controllers文件夹中。
To create an Atom feed using official libraries that are part of the .NET work framework, you first need to add a reference to System.ServiceModel in the new AccountManagement.RegularAccounts.BeganFollowing project. After setting up a route definition, shown in Listing 13-21, you can then use classes from System.ServiceModel to create an Atom feed using events retrieved from the Event Store, as shown in Listing 13-22. The code in Listing 13-22 needs to be added as a new controller in the Controllers folder.
如清单 13-22 所示,使用SyndicationFeed类创建了一个 Atom feed。然后,通过XmlWriter将创建的 feed 设置为 HTTP 请求的响应。在响应对象上,将application/atom+xml设置为内容类型。这将直接作为 HTTP Content-Type响应头的值传递。还可以看到,从事件存储(EventRetriever.RecentEvents())被转换为FeedItems。但是清单 13-22 中没有展示如何从事件存储中检索事件。这将在下文展示。
As you can see in Listing 13-22, an Atom feed is created using the SyndicationFeed class. The created feed is then set as the response of the HTTP request via an XmlWriter. On the response object, application/atom+xml is set as the content type. This will be passed directly as the value for the HTTP Content-Type response header. You can also see that individual events, retrieved from the Event Store (EventRetriever.RecentEvents()), are converted into FeedItems. But what you can’t see in Listing 13-22 is how to retrieve the events from the Event Store. That is shown next.
在清单 13-22 中,通过使用自定义实用程序类EventRetriever从事件存储中检索事件来生成各个 feed 项。EventRetriever的内容如清单 13-23 所示,需要将其添加到项目中。为了方便起见,如果您不想创建另一个文件,可以将其放在包含BeganFollowingController的文件底部。
In Listing 13-22, individual feed items are generated by retrieving events from the Event Store using a custom utility class: EventRetriever. The contents of EventRetriever are shown in Listing 13-23 and need to be added to your project. To make life easy, you can pop it in the bottom of the file containing the BeganFollowingController if you don’t want to create another file.
EventRetriever是一个包装 Event Store C# 客户端的实用程序类。它被硬编码用于检索过去 20 个事件,从最近的事件开始。这可以通过使用ReadStreamEventsForward来实现,该函数从最近的事件开始,然后向后执行。Event Store C# 客户端提供的许多功能本书并未涵盖,因此,如果您正在考虑使用 Event Store 和 C# 客户端,Event Store 网站包含大量实用信息。
EventRetriever is a utility class that wraps the Event Store C# client. It is hard-coded to retrieve the past 20 events, starting from the most recent. This is enabled by using ReadStreamEventsForward, which starts with the most recent events and works backward. There’s a lot of functionality provided by the Event Store C# client that isn’t covered in this book, so if you’re thinking about using the Event Store and the C# client, the Event Store website contains lots of useful information.
为了使EventRetriever能够编译,您还需要在此项目中添加对 Event Store C# 客户端的引用。以下命令将负责安装它:
For the EventRetriever to compile, you need to add a reference to the Event Store C# client in this project as well. The following command takes care of installing it for you:
Install-Package EventStore.Client -Project AccountManagement.RegularAccounts.BeganFollowing
Install-Package EventStore.Client -Project AccountManagement.RegularAccounts.BeganFollowing
要测试 Atom feed 是否按预期工作,首先需要更新入口点资源(在AccountManagement.EntryPoint.Api项目中),以提供指向 Atom feed 的链接(记住,客户端不应该与资源耦合,而应该与入口点耦合)。清单 13-24 显示了包含所需链接的更新后的入口点资源。将资源添加到您的项目,您可以通过直接在浏览器中查看来测试提要(如果您只是测试它,直接访问资源是可以的):http://localhost:4102/accountmanagement/beganfollowing。
To test that your Atom feed is working as expected, you first need to update the entry point resource (in the AccountManagement.EntryPoint.Api project) to provide a link to the Atom feed (remember, clients should not be coupled to resources, only the entry point). Listing 13-24 shows the updated entry point resource containing the required link. Once the resource is added to your project, you can test the feed by viewing it directly in a browser (accessing a resource directly is okay if you’re just testing it): http://localhost:4102/accountmanagement/beganfollowing.
在一个高度可扩展且可能拥有数百万用户的系统中,每秒可能会有数百或数千个事件。如果用一个 Atom feed 来存储所有这些事件,很快就会变得无法使用。这可能会导致大量的网络带宽浪费,以及其他与效率低下相关的问题。一个常见的解决方案是每个 feed 显示固定数量的事件,一旦达到容量上限,就将 feed 归档。重要的是,每个 feed 都包含指向上一个和下一个归档(如果存在)的超媒体链接。欲了解更多信息,请参阅互联网工程任务组 (IETF) 发布的题为“Feed 分页和归档”的征求意见稿 (RFC) ( https://tools.ietf.org/html/rfc5005 )。
In a highly scalable system with potentially millions of users, there may be hundreds or thousands of events every second. Having a single Atom feed for all these events would quickly become unusable. This could result in a massive waste of network bandwidth, as well as other inefficiency-related issues. A common solution is to display a fixed number of events per-feed, and once a capacity is reached to then archive the feed. Importantly, each feed contains hypermedia links to the previous and next archives (if they exist). For more information, the Internet Engineering Task Force (IETF) has a request for comments (RFC) titled “Feed Paging and Archiving” (https://tools.ietf.org/html/rfc5005).
使用公开领域事件的 Atom 源类似于在消息系统中订阅消息。然而,使用 Atom 源会反转接收推送消息的过程,改为轮询和拉取消息。对于开发人员来说,这需要更多的前期工作,但它确实具有显著的优势。
Consuming an Atom feed that exposes domain events is akin to subscribing to messages in a messaging system. However, consuming an Atom feed inverts the process of receiving pushed messages by polling and pulling them instead. It’s a little more work up-front for developers, but it definitely has compelling advantages.
在创建 Atom 源的消费者时,您可以再次利用 Atom 的流行优势,使用 .NET 框架中的官方库。在构建 Discovery 限界上下文的第一部分(用于轮询 Began Following Atom 源)时,您很快就会看到这一点。为此创建的项目不必是 Web 项目。相反,您可以创建一个名为Discovery.Recommendations.Followers的新 C# 类库(如图 13.10中的容器图所示)。与之前一样,要利用 .NET 的简易聚合 (RSS) 库,您需要添加对System.ServiceModel的引用。您还需要将此项目配置为启动项目。
When creating a consumer of an Atom feed, you can again take advantage of Atom’s popularity by using official libraries in the .NET framework. You’ll see this shortly as you build the first part of the Discovery bounded context that polls the Began Following Atom feed. The project you create for this does not need to be a web project. Instead, you can create a new C# Class Library called Discovery.Recommendations.Followers (as per the containers diagram in Figure 13.10). As before, to take advantage of .NET’s really simple syndication (RSS) libraries, you need to add a reference to System.ServiceModel. You also need to configure this project as a start-up project.
在新的轮询组件中,您即将添加的逻辑由几个通用步骤组成。这些步骤与您构建的任何 Atom 信息流轮询应用程序中的步骤可能类似。
Inside the new polling component, the logic you are about to add consists of a few generic steps. These steps are likely to be similar in any Atom-feed polling application you build.
清单 13-25 展示了为 Discovery 有界上下文实现轮询消费者的第一部分。这仅包含高级逻辑。您可以将所有这些代码添加到项目根目录中名为BeganFollowingPollingFeedConsumer的单个类中。
The first part of implementing the polling consumer for the Discovery bounded context is shown in Listing 13-25. This contains only the high-level logic. You can add all this code to your project inside a single class called BeganFollowingPollingFeedConsumer in the root of the project.
清单 13-25 展示了 feed 消费者的第一部分。它演示了如何从 feed 中检索并处理一批事件。可以看到,通过调用Thread.Sleep() ,轮询被设置为每秒最多一次。
Listing 13-25 shows the first part of the feed consumer. It illustrates how a batch of events will be retrieved from the feed and processed. You can see polling is set to a maximum of once per second with the call to Thread.Sleep().
现在焦点转移到实际获取 feed 的底层细节。如清单 13-26 所示;它是一个 REST API 客户端的示例,它跟踪超媒体中的链接,从入口点到目标资源。您需要将此代码直接添加到清单 13-25 中添加的代码下方。您还需要通过运行以下命令将ServiceStack.Text添加到项目中:
Focus now shifts to the lower-level details of actually fetching the feed. This is shown in Listing 13-26; it’s an example of a REST API client following links in hypermedia from an entry point to a target resource. You need to add this code directly below the code you added from Listing 13-25. You also need to add ServiceStack.Text to the project by running the following command:
Install-Package ServiceStack.Text -Project Discovery.Reccommendations.Followers
Install-Package ServiceStack.Text -Project Discovery.Reccommendations.Followers
清单 13-26 中的代码还依赖于清单 13-27 中的类和以下using语句,这些语句需要在同一个文件中添加:
The code in Listing 13-26 also depends on the classes in Listing 13-27 and the following using statements, which need to be added in the same file:
using ServiceStack.Text;
using System.Xml.Linq;
using ServiceStack.Text;
using System.Xml.Linq;
获取 feed 后,就可以处理各个事件了。清单 13-28 展示了BeganFollowingPollingFeedConsumer的一个示例实现。
After fetching the feed, individual events can then be processed. A demonstrative implementation of this for the BeganFollowingPollingFeedConsumer is shown in Listing 13-28.
清单 13-28 演示了在 feed 消费者中可能看到的通用高级逻辑。首先,从 feed 中获取一批事件,然后选择尚未处理的事件。在某些情况下,如果 feed 需要分页或存档,您可能需要再次使用超媒体发出额外的请求,以定位最后处理的事件。找到未处理的事件后,您可以根据域规则处理每个事件,并更新最后处理的事件的 ID。
Listing 13-28 demonstrates the generic high-level logic you would likely see in a feed consumer. First you fetch a batch of events from the feed, then you select the ones that have not yet been processed. In some cases where feeds are paged or archived, you may need to make additional requests, again using hypermedia, to locate the last event processed. After locating the events that are unprocessed, you then process each one according to your domain rules and update the ID of the last processed event.
您可能想知道在事件处理过程中如何处理错误。基于 REST 的集成不支持有害消息或传递性消息。本章末尾将更详细地介绍这一点。
You may be wondering how errors are handled during the processing of events. With REST-based integration, there is no out-of-the-box support for poison messages or transitive messages. This is covered in a touch more detail toward the end of the chapter.
要完成示例,您需要实现剩余的底层逻辑,即解析来自 feed 的事件。如清单 13-29 和 13-30 所示。您还需要最后一对using语句:
To complete the example, you need to implement the remaining piece of lower-level logic, which parses events from the feed. This is shown in Listings 13-29 and 13-30. You also require a final pair of using statements:
using System.IO;
using System.Xml;
using System.IO;
using System.Xml;
本章的示例到此结束。希望您已经理解了足够的理论,并看到了足够多的示例,从而有信心将 REST 作为项目中有界上下文集成的一种选择。
That wraps up the example for this chapter. Hopefully you’ve understood enough theory and seen enough examples to feel confident about considering REST as an option for bounded context integration on your projects.
本示例剩下的工作就是测试一切是否正常。您可以通过 POST 新的关注者信息来测试,如前所示。留意自动弹出的控制台窗口。您应该会看到类似于图 13.21的输出。您也可以再次直接在浏览器中访问 Atom 提要,查看其中出现的新事件。
All that remains for this example is to test that everything works. You can do that by POSTing new followers, as shown previously. Keep an eye on the console window that automatically pops up. You should see output similar to Figure 13.21. You can also access the Atom feed directly in the browser again to check the new events that appear on it.
图 13.21 Feed 消费者处理事件
FIGURE 13.21 Feed consumer processing events
与消息传递系统或任何其他系统一样,应用程序在初始部署后必须提供支持。这可能涉及 API 的版本控制(随着 API 的演进)、系统性能监控,或用于业务决策的指标收集。
As with a messaging or any other system, you have to support the application after it has been initially deployed. This may involve versioning APIs as they evolve, monitoring how the system is performing, or capturing metrics that are used to inform business decisions.
API 的小改进可以轻松实现,且不会破坏任何现有客户端。关键在于确保更改向后兼容。如果您有一个生成运输状态资源的应用程序:
Small improvements to APIs can easily be achieved without breaking any existing clients. The key is to make sure changes are backward compatible. If you had an application that produced the Shipping Status resource:
{
"totalLegs": 5,
"legsCompleted": 3,
"currentVesselId":"sst399",
"nextVesselId":"u223a"
}
{
"totalLegs": 5,
"legsCompleted": 3,
"currentVesselId":"sst399",
"nextVesselId":"u223a"
}
并想向其中添加一条新信息,您只需在底部添加额外的信息,如下所示:
and wanted to add a new piece of information to it, you need only add the extra piece of information at the bottom like this:
{
"totalLegs": 5,
"legsCompleted": 3,
"currentVesselId":"sst399",
"nextVesselId":"u223a",
"eta":"2014-09-01"
}
{
"totalLegs": 5,
"legsCompleted": 3,
"currentVesselId":"sst399",
"nextVesselId":"u223a",
"eta":"2014-09-01"
}
这是一个向后兼容的变化,并且是可取的,因为与旧格式结合的客户端不会中断。
This is a backward-compatible change and is desirable because clients coupled to the old format do not break.
API 大修是一个颇具争议的话题。当您想对 API 进行重大或重大更改时,就会发生这种情况。您可能想要删除资源、在资源之间移动信息,或者彻底更改格式。两种最常见的版本控制选项是将版本信息包含在 URI 或 HTTP 标头中。对 URI 进行版本控制通常需要使用前缀,例如 /v2/accountmanagement/。或者,使用标头进行版本控制可能涉及使用 HTTP Version标头,如下所示:Version: 2。
API overhauls are a more contentious topic. These occur when you want to make big or breaking changes to an API. You may want to remove resources, move information between resources, or completely change formats. The two most common versioning options are to include the version in the URI or in an HTTP header. Versioning a URI usually involves a prefix such as /v2/accountmanagement/. Alternatively, versioning with a header may involve using the HTTP Version header like this: Version: 2.
使用 HTTP 的一大优势在于,您可以轻松将许多现成的监控工具插入 API,立即获得大量指标。New Relic ( http://newrelic.com/ ) 是一个热门选择,但它并非免费。您可以结合使用自定义指标,或者选择其他工具来捕获自定义指标。在这种情况下,StatsD(https://github.com/etsy/statsd/)和 C# StatsD 客户端(https://github.com/goncalopereira/statsd-csharp-client)等工具是流行的选择。
A big benefit of using HTTP is that there are a lot of off-the-shelf monitoring tools you can simply plug in to your APIs to immediately have a whole host of metrics. New relic (http://newrelic.com/) is a popular choice but it is not free. Instead, or in combination, you may want to capture custom metrics. In such cases, tools like StatsD (https://github.com/etsy/statsd/) and the C# StatsD client (https://github.com/goncalopereira/statsd-csharp-client) are popular options.
你可能已经开始形成自己的观点了,但对于想要构建可扩展、容错系统且不与消息传递框架耦合的团队来说,REST 绝对是一个不错的选择。不过,在决定 REST 是正确的选择之前,有必要先了解一下它的一些缺点。
You’ve probably started to form your own opinions now, but REST is definitely an option for teams that want to build scalable, fault-tolerant systems without being coupled to messaging frameworks. Before you decide that REST is the right choice, though, it’s important to discern a few of its drawbacks.
与消息传递系统相比,REST 的许多缺点都涉及更多的前期开发工作。使用 REST 构建可扩展、容错的系统可能需要更大的初始工作量。大部分额外的开发工作是为了弥补消息传递解决方案中开箱即用的功能。但是,在阅读这些缺点列表时,请记住,在项目的整个生命周期中,这些缺点可能会转化为优势。您需要管理的框架更少,并且在理解分布式系统的实际通信方式时,您将更接近核心。
A number of REST’s drawbacks when compared to messaging systems involve more development work up front. It can be a bigger initial effort to build scalable, fault-tolerant systems with REST. Most of the additional development work is to compensate for features that come out of the box with messaging solutions. But as you read through the list of drawbacks, keep in mind that over the lifetime of the project, the drawbacks may turn into advantages. You’ll have fewer frameworks to manage, and you will be closer to the metal when it comes to understanding how your distributed system actually communicates.
事件驱动的 REST 相比 RPC 提高了容错能力,但与消息传递解决方案相比略有不足。在上一章中,我们捕获了下单的意图并立即存储。任何PlaceOrder命令的传递失败都只会导致消息重试。但对于您在本章中构建的 REST 系统而言,情况并非如此。如果在尝试存储“开始关注”事件期间事件存储不可用,则事件存储恢复后不会自动恢复。
Event-driven REST improves fault tolerance compared to RPC but lacks a little compared to messaging solutions. In the previous chapter, intent was captured to place an order and immediately stored. Any failures delivering the PlaceOrder command would simply result in the message being retried. That’s not true for the REST system you built in this chapter. If the Event Store was unavailable during an attempt to store a Began Following event, there is no automatic recovery when the Event Store comes back online.
提高容错能力的一个选项是自行添加存储转发机制。这可能包括在容错能力对业务至关重要的位置添加队列。或者,您可以尝试高可用性方法,通过在负载均衡器后面或集群中添加更多应用程序实例。事件存储支持集群,因此对于本章的示例来说,这绝对是一个可行的选择。
One option to improve fault tolerance is to add store-and-forward mechanisms yourself. This could involve adding queues in locations where fault tolerance is important to the business. Alternatively, you could try a high-availability approach by adding more instances of an application behind a load balancer or to a cluster. The Event Store supports clustering, so that’s definitely a viable option for the example in this chapter.
总而言之,您必须付出更多努力才能获得消息传递框架默认提供的一些容错优势。
To summarize, you have to work a bit harder to gain some of the fault tolerance benefits that messaging frameworks provide by default.
不共享、松耦合且异步通信的系统总是很容易受到最终一致性的影响。本章推荐的事件驱动 REST 就属于此类。例如,当账户管理限界上下文暴露“开始关注”事件时,它已经将它们存储在本地。但是轮询 feed 的消费者在轮询 feed 并处理完新事件之前,不会立即获得更新。因此,根据客户端访问的 API,他们可能会看到或看不到基于近期事件的信息。
Share-nothing, loosely coupled systems that communicate asynchronously are always going to have a high susceptibility to eventual consistency. Event-driven REST as recommended in this chapter definitely falls into that category. For example, when the Account Management bounded context exposes Began Following events, it has already stored them locally. But consumers who poll the feed don’t immediately get updated until they have polled the feed and processed the new events. So, depending on which API clients hit, they may or may not see information based on recent events.
与 REST 集成时处理最终一致性依赖于与消息系统相同的基本概念。您需要放弃大型事务,转而使用小型事务。此外,您需要前滚到新的状态。最后,考虑多次重试消息,以期最终成功。
Dealing with eventual consistency when integrating with REST relies on the same fundamental concepts as in a messaging system. You need to forego big transactions in favor of smaller ones. Also, you need to roll forward into new states. Finally, consider retrying messages a number of times in the hope that eventually they will succeed.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for this Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 14 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 14 download and individually named according to the names throughout the chapter.
将实现模型映射回分析模型并确保它们相互绑定并非易事。为了指导开发人员并阐明设计,Evans 以 Martin Fowler 在其著作《企业应用程序架构模式》中首次提出的领域模型模式为基础,引入了一种模式语言,其中包含许多构建块模式,用于创建有效的领域模型。这些模式基于最佳实践和面向对象技术构建,有时被称为领域驱动设计 (DDD) 的战术模式。许多模式本身并不新鲜,但 Evans 是第一个以这种方式对它们进行分组的人,以帮助开发人员创建有效的领域模型。本章将对战术构建块模式进行高层次介绍领域驱动设计。本书的这一部分为每个模式都开辟了单独的章节,对每个模式进行了更详细的介绍。
Mapping the implementation model back to the analysis model and ensuring they are bound to one another is hard. To guide developers and clarify designs, Evans has built upon the domain model pattern that was first catalogued in Martin Fowler’s book Patterns of Enterprise Application Architecture. He introduces a pattern language containing a number of building block patterns to enable the creation of effective domain models. The patterns, built around best-practice, object-oriented techniques, are sometimes referred to as the tactical patterns of Domain-Driven Design (DDD). Many of the patterns themselves are not new, but Evans was the first to group them in this manner as an aid for developers to create effective domain models. This chapter gives a high-level introduction to the tactical building block patterns of Domain-Driven Design. Each pattern has its own chapter in this part of the book where it is covered in more detail.
虽然本章及本部分的其余部分详细介绍了创建领域模型的技术,但构建领域模型的实现策略应保持灵活性并开放创新。Evans 的原文倾向于面向对象的方法,但也不要忽视不同的建模范式,正如第五章“领域模型实现模式”中讨论的那样。您将在本章后面读到的领域事件模式并未包含在最初的构建块中,Evans 最近表示对此感到遗憾。此外,函数式编程和事件溯源(第 18 章“领域事件”中介绍)正成为表达领域模型的流行方式。
Although this chapter and the rest of this part detail techniques for creating domain models, the implementation tactics for building domain models should remain flexible and open to innovation. Evans’s original text favored an object-oriented approach, but don’t overlook the different modeling paradigms, as discussed in Chapter 5, “Domain Model Implementation Patterns.” The domain events pattern, which you will read about later in this chapter, was not included in the original building blocks, something that Evans recently said he regretted. In addition, functional programming and event sourcing (covered in Chapter 18, “Domain Events”) are becoming popular ways to express domain models.
自 Evans 的原始著作以来,用于创建领域模型并将实现与分析联系起来的模式一直在不断发展。创建领域模型的语义可以改变,并且将会改变,重要的是使用领域语言——通用语言(Ubiquitous Language)在代码中表示概念。
The patterns used to create domain models and tie implementation to analysis have continually evolved since Evans’s original text. The semantics of how you create domain models can and will change, what is important is to represent concepts in code using the language of the domain—the Ubiquitous Language.
DDD 中的战术模式的作用是管理复杂性并确保领域模型中行为的清晰度。您可以使用这些模式来捕获和传达领域内的含义、关系和逻辑。这些模式基于可靠的面向对象原则构建,许多模式都收录在广受好评的设计书籍中,例如Martin Fowler 撰写的《企业应用程序架构模式》和Ralph Johnson、John Vlissides、Richard Helm 和 Erich Gamma 撰写的《设计模式:可复用面向对象软件的元素》 。
The role of the tactical patterns in DDD is to manage complexity and ensure clarity of behavior within the domain model. You use the patterns to capture and convey meaning, relationships, and logic within the domain. The patterns are built around solid object-oriented principles, and many are catalogued in widely regarded design books, namely Patterns of Enterprise Application Architecture, by Martin Fowler, and Design Patterns: Elements of Reusable Object-Oriented Software, by Ralph Johnson, John Vlissides, Richard Helm, and Erich Gamma.
每个构建块模式都旨在承担单一职责;它可以表示领域中的概念,例如实体或值对象;也可以确保领域概念与生命周期问题(例如工厂对象或存储库对象)保持清晰的联系。在某种程度上,您可以将构建块视为一种通用语言 (UL),供开发人员用作构建丰富实用的领域模型的框架。
Each building block pattern is designed to have a single responsibility; it could be to represent a concept in the domain like an entity or a value object, or it could be to ensure that the concepts of the domain are kept uncluttered from lifecycle concerns like the factory or repository objects. In a way, you can view the building blocks as a ubiquitous language (UL) for developers to use as a framework for constructing rich and useful domain models.
在创建领域模型时,你可以使用多种构建块模式,如图 14.1所示。请注意,应用服务模式是领域模型的客户端,因此本书的这一部分不予介绍。应用服务将在第 25 章“命令:用于处理业务用例的应用服务模式”中介绍。
You can use numerous building block patterns, shown in Figure 14.1, in the creation of a domain model. Note that the application services pattern is a client of the domain model and is therefore not covered in this part of the book. Application services are covered in Chapter 25, “Commands: Application Service Patterns for Processing Business Use Cases.”
图 14.1战术模式——领域模型构建块。
FIGURE 14.1 Tactical patterns—domain model building blocks.
以下模式代表了问题域内的策略和逻辑。它们表达了对象之间的关系、模型规则,并将分析模型的细节与代码实现模型绑定。这些模式用代码表达了模型的元素。
The following patterns represent the policies and logic within the problem domain. They express relationships between objects, model rules, and bind the detail of the analysis model to the code implementation model. These are the patterns that express the elements of your model in code.
实体代表领域中的概念,该概念由其标识而非属性定义。虽然实体的标识在其整个生命周期内保持不变,但其属性可能会发生变化。实体负责定义“相同”的含义;在代码中,这通常通过重写类的相等操作来实现。
An entity represents a concept in your domain that is defined by its identity rather than its attributes. Although an entity’s identity remains fixed throughout its lifecycle, its attributes may change. An entity is responsible for defining what it means to be the same; in code this is often achieved by overriding the equality operations of a class.
实体的一个例子是产品;它的唯一标识一旦设置就不会改变,但它的描述、价格等可以多次更改。实体是可变的,因为属性可以改变。
An example of an entity is a product; its unique identity won’t change once it is set but its description, price, etc., can be altered many times. Entities are mutable as the attributes can change.
图 14.2显示了实体的主要概念。
Figure 14.2 shows the main concepts of an entity.
图 14.2一个实体。
FIGURE 14.2 An entity.
在清单 14-1 中,您可以看到一个产品被建模为一个实体。
In Listing 14-1, you see a product modeled as an entity.
清单 14-1 显示,产品的标识符是在构造时设置的,并且没有方法可以更改它。Product 实体将所有工作委托给 Money 和 Option 值对象(即 Order 的属性/特征)。产品实体封装数据并公开行为;类的数据是隐藏的。
Listing 14-1 shows that the identifier of a product is set on construction and there are no methods to change it. The Product entity delegates all work to the Money and Option value objects—the attributes/characteristics of the Order. The product entity encapsulates data and exposes behavior; the data of the class is hidden.
您还会注意到,该实体有一个通用基类,它采用用于标识的类型。基类中重写了相等方法,类似于您在 Money 值对象中看到的。但是,这次比较对象时,您比较的是类型和 ID。
You will also have noticed that the entity has a generic base class that takes the type used for identification. Within the base class are the overridden equality methods, similar to what you saw in the Money value object. However, this time when comparing objects, you compare the type and the ID.
为了完整起见,清单 14-2 展示了实体基类的实现。
For completeness, Listing 14-2 shows the implementation of the Entity base class.
清单 14-2 表明,通过继承此基类,可以将判断实体之间相等性的逻辑保留在实体本身中。抽象基类将所有身份和相等性检查的干扰排除在实现类之外,以便实现类可以专注于业务逻辑。
Listing 14-2 shows that by inheriting from this base class, you keep the logic that determines equality between entities within the entity itself. The abstract base class keeps all the noise of identity and equality checking out of the implementation class so that it can focus on business logic.
值对象表示域中仅通过其特征而为人所知的元素或概念;它们用作模型中元素的描述符;它们不需要唯一的标识。由于值对象在模型中没有概念标识,因此它们由其属性定义;它们的属性决定了它们的身份。值对象不需要身份,因为它们始终与另一个对象关联,因此可以在特定上下文中理解。例如,您可能有一个订单实体,它使用值对象来表示订单送货地址、商品、快递信息等。这些特征中的任何一个都不需要身份本身,因为它只有在附加到订单的上下文中才有意义。未附加到订单的订单地址没有意义。值对象基于其属性进行比较,并且同类实体负责任何相等性检查。
Value objects represent the elements or concepts of your domain that are known only by their characteristics; they are used as descriptors for elements in your model; they do not require a unique identity. Because value objects have no conceptual identity within the model, they are defined by their attributes; their attributes determine their identity. Value objects don’t need identity because they are always associated with another object and are therefore understood within a particular context. For instance, you may have an order entity that uses value objects to represent the order shipping address, items, courier information, and so on. Not one of these characteristics needs identity itself because it only has meaning within the context of being attached to an order. An order address that is not attached to an order does not have meaning. Value objects are comparable based on their attributes, and like entities are responsible for any equality checks.
由于值对象是由其属性定义的,因此它们被视为不可变的;也就是说,一旦构造,它们就永远不能改变其状态。值对象的一个很好的例子就是钱。你无法区分口袋里的五枚一英镑硬币和五张一美元钞票,这没关系。你不关心货币的身份,只关心它的价值和它所代表的东西。如果有人用一张五美元的钞票换你钱包里的一张,这不会改变你仍然有五美元的事实。当然,在现实生活中,钱可以有一个序列号形式的唯一标识符,但领域模型并不反映现实生活。相反,它是对它的抽象,是为了满足问题域内用例的需求而构建的。图 14.3展示了值对象的主要概念。
Because they are defined by their attributes, value objects are treated as immutable; that is, once constructed, they can never alter their state. A good example of a value object is money. It doesn’t matter that you can’t distinguish between the same five one pound coins or one dollar bills in your pocket. You don’t care about the currency’s identity—only about its value and what it represents. If somebody swapped a five dollar bill for one you have in your wallet, it would not change the fact that you still have five dollars. Of course, in real life, money can have a unique identifier in the form of a serial number, but the domain model does not reflect real life. Instead, it is an abstraction of it built to fulfill the needs of use cases within the problem domain. Figure 14.3 shows the main concepts of a value object.
图 14.3值对象。
FIGURE 14.3 A value object.
清单 14-3 展示了将货币建模为价值对象的情况。
Listing 14-3 shows money modeled as a value object.
从清单 14-3 中可以看出,相等性方法已被重写,这意味着 Money 对象仅通过其属性进行比较;在本例中,属性是值和货币。Money 类是不可变的。一旦创建,它就不能改变状态。add 方法返回一个 Money 对象的新实例。这被称为操作闭包,因为你没有改变原始 Money 对象的状态。另一个值对象用于捕获货币的概念。在比较相等性时,你将操作委托给货币对象。同样,相等性方法已被重写。
As you can see from Listing 14-3 the equality methods have been overridden, meaning that the Money object is compared only by its attributes; in this instance, the attributes are the value and the currency. The money class is immutable. Once it has been created, it cannot change state. The add method returns a new instance of a Money object. This is known as closure of operations because you are not altering the state of the original money object. Another value object is used to capture the concept of currency. You delegate to the currency object when comparing equality. Again, the equality methods have been overridden.
领域服务封装了领域逻辑和概念,这些逻辑和概念在模型中无法自然地建模为值对象或实体。领域服务没有身份或状态;它们的职责是使用实体和值对象来编排业务逻辑。领域服务的一个很好的例子是运费计算器,如清单 14-4 所示。该服务是一个业务功能,给定一组托运货物(值对象)和一组重量等级,它可以计算运费。此功能不适合放在领域对象上,因此最好将其表示为领域服务。
Domain services encapsulate domain logic and concepts that are not naturally modeled as value objects or entities in your model. Domain services have no identity or state; their responsibility is to orchestrate business logic using entities and value objects. A good example of a domain service is a shipping cost calculator as shown in Listing 14-4. This service is a business function that, given a set of consignments (value objects) and a collection of weight bandings, can calculate the cost of shipping. This functionality does not sit comfortably on a domain object, so it is better represented as a domain service.
ShippingCostCalculator封装了基于一组托运货物的重量以及单个箱子的重量来计算托运货物运输成本的领域逻辑。通过将此逻辑组织到特定的领域服务下并为其命名,您可以以简洁的方式与领域专家明确地讨论特定的领域逻辑,例如策略或流程。ShippingCostCalculator可能一直是业务部门隐含的概念,但通过为其命名,您确保了它现在已明确,并且应该添加到 UL 和分析模型中。
The ShippingCostCalculator encapsulates the domain logic that calculates the shipping cost of consignment based on the weight of a collection of consignments, along with the weight of a box. By organizing this logic under a specific domain service and naming it, you can explicitly talk to domain experts about a particular piece of domain logic such as a policy or a process in a concise manner. The ShippingCostCalculator may have been an implicit concept held by the business, but by naming it, you have ensured it is now explicit and should be added to the UL and the analysis model.
C# 中的模块以命名空间或项目的形式实现。您可以使用它们来组织和封装相关概念(实体和值对象),从而简化对更大领域模型的理解。模块用于分解领域模型。不要将它们与分解领域的子域和界定领域模型适用性的有界上下文混淆。如图 14.4所示,模块名称直接取自 UL,使领域模型的不同部分能够被单独理解。模块使开发人员能够在深入研究类文件之前快速阅读和理解代码中的领域模型。它们还充当责任边界,清晰地定义领域模型的各个部分,并确保将领域对象之间的关系保持在最低限度。应用模块可以促进领域模型的低耦合和高内聚。尝试将模块的内容限制为一个内聚的集合。
Modules in C# are implemented as namespaces or projects. You use them to organize and encapsulate related concepts (entities and value objects) so you can simplify your understanding of larger domain models. Modules are used to decompose the domain model. Don’t confuse them with subdomains that decompose the domain and bounded contexts that delimit the applicability of a domain model. As shown in Figure 14.4, module names are lifted straight from the UL and enable distinct parts of the domain model to be understood in isolation. Modules enable developers to quickly read and understand a domain model in code before digging deep into class files. They also act as a responsibility boundary, clearly defining parts of the domain model and ensuring that relationships between domain objects are kept to a minimum. Apply modules to promote low coupling and high cohesion within your domain model. Try limiting the contents of a module to a cohesive set.
图 14.4用于组织领域模型中的领域概念的模块。
FIGURE 14.4 Modules used to organize domain concepts within a domain model.
以下模式处理代表域结构的对象的创建和持久性。
The following patterns deal with the creation and persistence of the objects that represent the structure of the domain.
实体和值对象协作形成复杂的关系,以满足领域模型中的不变量。处理大型互连的对象关联时,通常很难在针对领域对象执行操作时确保一致性和并发性。图 14.5展示了一个大型对象图。试图将这些对象集合视为一个概念整体非常困难,并且可能会导致应用程序的性能问题。例如,我们不会仅仅因为客户先前订单的状态同时发生变化就阻止其更新账户地址。这两件事互不相关,不需要共享一致性或并发性边界。
Entities and value objects collaborate to form complex relationships that meet invariants within the domain model. When dealing with large interconnected associations of objects, it is often difficult to ensure consistency and concurrency when performing actions against domain objects. Figure 14.5 shows a large object graph. Trying to treat this collection of objects as one conceptual whole is difficult and could result in performance problems for an application. For example we would not want to block a customer updating an address on her account just because an earlier order’s status is being changed at the same time. These two things are unrelated and need not share a consistency or concurrency boundary.
图 14.5大型对象图。
FIGURE 14.5 A large object graph.
领域驱动设计采用聚合模式来确保一致性,并定义对象图的事务并发边界。大型模型按不变量拆分,并分组为实体和值对象的聚合,这些聚合被视为一个概念整体。如图 14.6所示,您可以将模型提炼为聚合。
Domain-Driven Design has the Aggregate pattern to ensure consistency and to define transactional concurrency boundaries for object graphs. Large models are split by invariants and grouped into aggregates of entities and value objects that are treated as a conceptual whole. As shown in Figure 14.6, you can distill the model into aggregates.
图 14.6分解为聚合的大对象图。
FIGURE 14.6 A large object graph split into aggregates.
聚合根之间的关系应该通过保存对另一个聚合根 ID 的引用来实现,而不是通过保存对对象本身的引用,如图14.7所示。此原则有助于保持聚合之间的边界,并避免加载不必要的大型对象图。
Relationships between aggregate roots should be implemented by keeping a reference to the ID of another aggregate root and not a reference to the object itself, as shown in Figure 14.7. This principle helps to keep a boundary between aggregates and avoids the need to load large object graphs that are not required.
图 14.7聚合根充当聚合的入口点。
FIGURE 14.7 An aggregate root acts as the entry point to the aggregate.
图 14.6和 14-7中的聚合分组乍一看似乎是拆分对象图的合理方法;然而,仅仅基于相关概念定义聚合组只能在一定程度上改善一致性和并发性挑战。以客户聚合为例,如果在修改地址的同时修改了客户的某些个人信息,则可能会引发阻塞问题。这两个概念虽然与客户相关,但不需要不变式。因此,我们可以将它们拆分为两个独立的聚合,并对信用卡使用相同的逻辑,然后将它们全部分组到客户模块下,如图14.8所示。
The aggregate groupings in Figure 14.6 and 14-7 at first glance look like a reasonable way to split the object graph; however, defining aggregate groups based purely on related concepts only goes some way to improve consistency and concurrency challenges. Take the customer aggregate, if an address is amended at the same time that some personal details of a customer are changed we could introduce blocking issues. These two concepts although related to a customer do not require an invariant. Therefore, we can split these into two separate aggregates and use this same logic for a credit card, then group them all under a customer module, as can be seen in Figure 14.8.
图 14.8聚合应该基于不变量。
FIGURE 14.8 Aggregates should be based around invariants.
如图 14.9所示,聚合根充当聚合的入口点。聚合外部的任何其他实体或值对象都不能引用聚合内部的对象。聚合外部的对象只能引用另一个聚合的聚合根。对聚合内部对象的任何更改都必须通过聚合根进行。聚合根封装了聚合的数据,并且只暴露修改数据的行为。
An aggregate root, shown in Figure 14.9, acts as the entry point into the aggregate. No other entity or value object outside of the aggregate can hold a reference to an object within the aggregate. Objects outside the aggregate can only reference the aggregate root of another aggregate. Any changes to objects in the aggregate need to come through the root. The root encapsulates the data of the aggregate and only exposes behaviors to change it.
图 14.9聚合根充当聚合的入口点。
FIGURE 14.9 An aggregate root acts as the entry point to the aggregate.
如果创建实体或值对象足够复杂,则应该将构造过程委托给工厂。工厂确保在域对象创建之前满足所有不变量。已创建。如果领域对象很简单,并且没有特殊的有效构造规则,则优先使用构造函数方法而不是工厂对象。从持久存储重新创建领域对象时,也可以使用工厂方法。
If the creation of an entity or a value object is sufficiently complex, you should delegate the construction to a factory. A factory ensures that all invariants are met before the domain object is created. If a domain object is simple and has no special rules for valid construction, favor a constructor method over a factory object. You can also use factories when re-creating domain objects from persistent storage.
在清单 14-5 中,您可以看到客户实体具有工厂方法,可以使用有效的客户 ID 创建地址。
In Listing 14-5, you see that a Customer entity has a factory method to enable an address to be created with a valid customer ID.
领域模型需要一种用于持久化和补充聚合的方法。由于聚合被视为原子单元,因此如果不持久化整个聚合,则无法持久化对聚合的更改。如图 14.11所示,存储库是一种从模型中抽象出底层持久性存储的模式,使您可以创建模型而无需考虑基础架构问题。存储库是您应该用来检索和持久化聚合的机制。对于视图渲染,存储库不是必需的,而查询数据存储是满足报表需求的最有效方法。存储库涉及基础架构问题,因此并不总是需要抽象出底层框架完成了所有繁琐的工作。依靠 ORM(对象关系映射)框架作为存储库可能更有价值;例如 NHibernate、RavenDB 和 Entity Framework。许多开发人员对这种模式感到困惑。可以简单地将其视为一种持久化和 rehydration 的方法。将其视为基础设施,不要纠结于将其抽象化。
A domain model needs a method for persisting and hydrating an aggregate. Because an aggregate is treated as an atomic unit, you should not be able to persist changes to an aggregate without persisting the entire aggregate. A repository, as shown in Figure 14.11, is a pattern that abstracts the underlying persistence store from the model allowing you to create a model without thinking about infrastructure concerns. The repository is the mechanism that you should use to retrieve and persist aggregates. For view rendering, a repository is not required, and querying against a data store is the most efficient method for reporting needs. A repository is an infrastructure concern, so it is not always necessary to abstract away the underlying framework doing all the hard work. It can be more worthwhile to lean on ORM (Object Relational Mapper) frameworks to act as a repository; examples include NHibernate, RavenDB, and Entity Framework. Many developers get hung up on this pattern. Think of it simply as a method of persistence and rehydration. Treat it like infrastructure, and don’t get hung up on abstracting it away.
图 14.10一家工厂。
FIGURE 14.10 A factory.
图 14.11存储库。
FIGURE 14.11 A repository.
自 Eric Evans 的原始著作以来,出现了两种可用于创建领域模型的模式。即第 18 章介绍的领域事件模式和第 22 章(“事件源”)介绍的事件源模式。
Since Eric Evans’ original text, two patterns have emerged that are useful for creating domain models. Namely the domain events pattern, covered in Chapter 18, and the event sourcing pattern, covered in Chapter 22 (“Event Sourcing”).
领域事件表示业务关注的问题域中发生的事件。您可以使用事件以审计方式记录模型的变更,也可以将其用作跨聚合通信的一种形式。通常,对单个聚合根的操作可能会导致超出聚合根边界的副作用。模型中的其他聚合可以监听事件并采取相应的措施。
Domain events signify something that has happened in the problem domain that the business cares about. You can use events to record changes to a model in an audit fashion, or you can use them as a form of communication across aggregates. Often an operation on a single aggregate root can result in side effects that are outside the aggregate root boundary. Other aggregates within the model can listen for events and act accordingly.
例如,考虑电商网站中的购物篮,如图14.12所示。每当顾客将商品放入购物篮时,更新网站上显示的推荐商品非常重要。在这种情况下,在触发包含购物篮详细信息的领域事件后,客户的推荐会被修改。该事件由推荐的有界上下文订阅。如果不使用领域事件,则需要将购物篮有界上下文与推荐上下文明确地耦合。领域事件提供了更自然的沟通流程,并专注于“何时”。
For example, consider the basket within an e-commerce site, as shown in Figure 14.12. Every time a customer places an item in a basket, it’s important to update the recommended products that are displayed on the site. In this scenario, after a domain event is raised with details of the basket, a customer’s recommendations are modified. The event is subscribed to by the recommendation’s bounded context. Without using a domain event, you need to explicitly couple the basket bounded context to the recommendation context. Domain events give a more natural flow of communication and focus on the when.
图 14.12领域事件。
FIGURE 14.12 A domain event.
在清单 14-6 中,您可以看到从篮子中发布的域事件。
In Listing 14-6, you see a domain event being published from a basket.
在清单 14-7 中,您可以看到一个推荐服务处理由购物篮引发的事件。
In Listing 14-7, you see a recommendation service handling the event raised by the basket.
在这个阶段,不要太担心代码的语法;它将在第 18 章中介绍。领域事件在代码中起着至关重要的作用。
Don’t worry too much about the syntax of the code at this stage; it is covered in Chapter 18. Domain events play a crucial role in the code.
事件溯源是传统仅基于快照的持久化方案的一种流行替代方案。它不是将实体的状态存储在数据库中,而是存储导致该状态的一系列事件。存储所有事件可以提升企业的分析能力。企业不仅可以查询实体的当前状态,还可以查询过去任何时间点的状态,如图14.13所示。
A popular alternative to traditional snapshot-only persistence is event sourcing. Instead of storing the state of an entity in a database, you store the series of events that lead up to the state. Storing all of the events increases the analytical capabilities of a business. Instead of just asking what the current state of an entity is, a business can ask what the state was at any time in the past, as shown in Figure 14.13.
图 14.13存储事件,而不是快照
FIGURE 14.13 Storing events, not snapshots
能够查询过去任何时间点的领域模型状态,可以带来竞争优势,因为您可以将现实世界中发生的事件与领域模型状态的变化关联起来。在线旅行社可能想调查某个月预订数量大幅下降的原因。借助事件溯源,他们可以重建目录状态,并重新运行用户的搜索记录,以了解这些用户为何没有找到他们想要的假期。
Being able to query the state of your domain model at any time in the past provides a competitive business advantage because you can correlate events that have occurred in the real world with changes to the state of your domain model. Online travel agents may want to investigate why the number of bookings had a massive dip in a certain month. With event sourcing, they can rebuild the state of their catalog and re-run the searches that their users made to understand why those users did not find a vacation they wanted.
作为 DDD 从业者,你不仅有责任向业务部门建议使用事件溯源的可能性,还必须学习新的领域建模方法以支持事件溯源。具体来说,你需要放弃使用 ORM 将实体状态转储到数据库中的做法。相反,你需要找到一种建模、捕获和持久化领域数据的方法。事件。您甚至可能需要向现有数据库添加事件存储功能。所有这些问题都将在第22章中详细介绍。
As a DDD practitioner, not only is your responsibility to suggest the possibility of using event sourcing to the business, but you also have to learn new ways of modeling your domain to support event sourcing. In particular, you need to move away from dumping an entity’s state into a database using an ORM. Instead, you need to find a way of modeling, capturing, and persisting domain events. You may even need to add event store functionality to an existing database. All of these concerns are covered in detail in Chapter 22.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载位于www.wrox.com/go/domaindrivendesign 的“下载代码”选项卡上。代码位于第 15 章的下载中,并根据整章中的名称单独命名。领域模型包含实体,这些实体类似于电影中的角色。就像电影中的角色一样,通常是它们的属性使实体变得有趣或有用。你可能会注意到,詹姆斯·邦德的一个特点是他具有很强的魅力。同样,如果一个BankAccoun实体具有较大的Balance属性,你可能会觉得它很有趣。对这些重要的描述性属性进行建模是领域驱动设计 (DDD) 结构(称为值对象)的作用。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 15 download and individually named according to the names throughout the chapter. A domain model contains entities, which are akin to characters in a movie. And just like characters in a movie, it’s often their attributes that make entities interesting or useful. You might note that an attribute of James Bond is that he has a high level of charisma. Equally, you might find a BankAccount entity interesting if it has a large Balance attribute. Modeling these important descriptive attributes is the role of a Domain-Driven Design (DDD) construct known as the value object.
值对象没有身份标识。它们纯粹用于描述实体的领域相关属性,通常以某种数量的形式出现。无需处理身份标识通常会让使用值对象变得相对轻松愉快。特别是,不可变和可组合这两个特性支撑了它们的易用性。在本章中,您将了解这些特性以及其他相关特性。您还将看到一些常见的值对象建模模式,它们可以提升可用性和表达能力。
Value objects have no identity. They are purely for describing domain-relevant attributes of entities, usually in the form of some quantity. Having no identity to deal with often makes working with value objects relatively pain free and enjoyable. In particular, being immutable and combinable are two characteristics that support their ease of use. You will learn about these characteristics, along with other pertinent ones, in this chapter. You will also see some common value object modeling patterns that promote usability and expressiveness.
使用值对象有时仍然会很有挑战性,因为有一些问题需要更深入的思考:特别是持久化、验证和避免原始类型。本章也会涵盖这些挑战,以确保您了解创建自己的值对象所需的一切。
Working with value objects can still be challenging at times because there are a few considerations that require deeper thinking: notably, persistence, validation, and primitive avoidance. This chapter covers these challenges, too, to ensure that you know everything you need to be able to create value objects of your own.
值对象是实体的状态,描述实体或其拥有的事物。船舶可能有最大载货量,杂货店可能有库存水平,财务报告可能有季度营业额。这些关系都可以建模为实体-值对象关系。对于每个示例,请注意值对象如何表示具有度量、量级或值的特定概念——因此称为值对象。两个关键原理使值对象成为 DDD 中重要的技术结构。
Value objects are an entity’s state, describing something about the entity or the things it owns. A ship may have a maximum cargo capacity, a grocery may have a stock level, and a financial report may have quarterly turnovers. Each of these relationships would likely be modeled as an entity-value object relationship. For each example, note how the value object represents a particular concept that has a measurement, magnitude, or value—hence, value object. Two key rationales make value objects an important technical construct in DDD.
如果你仔细观察值对象,例如Money以及本章中介绍的其他对象,你会发现它们之间存在一个逻辑上的共性。它们拥有身份标识是没有意义的。只有实体才可以拥有身份标识。一个基本的例子就是银行账户;你几乎肯定需要通过 ID 来查找BankAccount实体,但你会通过 ID 来查找它的余额吗?在大多数领域,你不会这样做,因为余额本身没有任何意义或重要性。
If you look purposefully at value objects, like Money and others shown throughout this chapter, you will see a logical commonality. It would not make sense for them to have an identity. Only entities may have an identity. A basic example of this is a bank account; you would almost certainly need to look up a BankAccount entity by its ID, but would you look up its balance by its ID? In most domains, you wouldn’t because the balance has no meaning or importance in isolation.
清单 15-1 展示了一个BankAccount实体,其余额由Money对象表示。经过前面的讨论,希望您能清楚地了解为什么这种建模决策是合理的。
Listing 15-1 shows a BankAccount entity whose balance is represented by a Money object. After the previous discussion, hopefully it’s clear why this modeling decision is sensible.
如果一个概念缺乏明显的标识,那么这很可能表明它应该是你模型中的一个值对象。在本章后面,你将通过展示更多值对象的示例来更好地理解这一点。尤其重要的是,理解它们的定义特征是成功使用值对象的关键。
When a concept lacks an apparent identity, it’s a big clue that it should be a value object in your model. Later in the chapter, you will gain a better understanding of this when further examples of value objects are shown. In particular, understanding their defining characteristics is key to being successful with value objects.
DDD 的核心在于显式地传达重要的业务规则和领域逻辑。相反,像整数和字符串这样的原始类型本身并不擅长于此。虽然可以用原始类型来表示描述性概念,但大多数 DDD 从业者强烈反对这样做。相反,原始类型应该被包装成内聚的值对象,这些值对象可以显式地表示它们所建模的概念。
DDD is all about explicitly communicating important business rules and domain logic. Conversely, primitive types like integers and strings in isolation aren’t great at this. Although it is possible to represent descriptive concepts with primitive types, most DDD practitioners strongly discourage it. Instead, primitives should be wrapped up into cohesive value objects that explicitly represent the concepts they are modeling.
清单 15-2 展示了一个表示在线拍卖中当前中标价的对象。为了表示出价金额(以货币为单位),我们使用了一个整数。
Listing 15-2 shows an object representing the current winning bid in an online auction. To represent the price of the bid, in money, an integer is used.
在示例 15-2 中,将中标金额表示为整数并非最佳设计选择,主要有两个原因。其一,整数无法表达该领域中的价格——它没有将输入限制在允许的取值范围内,也没有表达计量单位或货币。这会产生巨大的歧义,隐藏了该领域的重要细节。
Representing the amount of the winning bid as an integer in Listing 15-2 is a sub-optimal design choice for two major reasons. One reason is that the integer does not express what a price is in this domain—it doesn’t restrict inputs to the allowed range of values, and it doesn’t express the unit of measurement or currency. This is a massive source of ambiguity that hides important details of the domain.
通过将中标金额建模为整数,还存在一个很大的风险,即相关领域概念将分散在整个领域中,而不是紧密地共置,因为您无法向原始类添加行为(这样做也没有意义)。
By modelling the winning bid amount as an integer, there is also a big risk that related domain concepts will be scattered throughout the domain rather than being cohesively co-located, because you cannot add behavior to primitive classes (nor would it make sense to).
在清单 15-3 中,Price值对象表明,在在线拍卖领域,增加中标价的规则非常重要。您可以看到,Price值对象的好处在于将所有与价格相关的行为(BidIncrement()和CanBeExceededBy())紧密地组织在一起。这有助于在该领域表达和执行价格概念的规则。每当讨论涉及价格时,开发人员和领域专家只需查看这个类就能理解价格是什么以及适用于价格的规则。
In Listing 15-3, the Price value object shows that in the online auction domain, the rules for incrementing the winning bid for a price are significant. You can see that the benefit of the Price value object is to cohesively group all the related behaviors of a price: BidIncrement() and CanBeExceededBy(). This helps to express and enforce the rules of the price concept in this domain. Whenever there is a discussion involving prices, developers and domain experts need only look at this one single class to understand what a price is and what rules apply to it.
如清单 15-3 所示,通过用领域概念包装原语,类型系统会为您完成从数据到领域概念的映射,从而消除清晰度不足的问题。因此,您与领域专家的沟通会更加清晰,领域概念对任何阅读代码的人来说也更加清晰易懂。此外,与单个概念相关的所有功能都具有内聚性建模。在本例中,用于增加价格和判断一个价格是否可以超过另一个价格的规则,就是与价格概念相关的行为的内聚性分组,这些行为应该并且确实存在共存。
As Listing 15-3 shows, by wrapping primitives with domain concepts, the type system does the mapping from data to domain concept for you, and the lack of clarity goes away. Consequently, your conversations with domain experts can be clearer and domain concepts will be more apparent to anyone reading the code. In addition, all functionality related to a single concept is modelled cohesively. In this instance, the rules for incrementing a price and determining if a price can be exceeded by another price are the cohesive grouping of behaviors related to the price concept that should and do live together.
清单 15-3 还说明了值对象通常应该如何进行细粒度处理。Price值对象本身将表示金钱的相关关注点推给了Money值对象,并使用Money的实例来表示中标金额。您将在本章后面看到Money值对象。
Listing 15-3 also illustrates how value objects should often be fine-grained. The Price value object itself pushes the related concern of representing money into the Money value object, using an instance of Money to represent the amount of the winning bid. You will the see Money value object later in this chapter.
值对象几乎是自包含的,这从根本上使它更容易使用。与函数式编程的支持者一样,DDD 从业者也喜欢值对象,因为它们不可变、无副作用且易于测试。正如您将在以下示例中看到的那样,它们确实还有一些其他值得注意的重要特征。
Being mostly self-contained is what makes value objects fundamentally easier to work with. Like proponents of functional programming, DDD practitioners are fond of value objects because they are immutable, side effect free, and easily testable. As you will see in the following examples, they do have a few other defining characteristics that are important to be aware of.
在学习值对象时,要记住的最重要的一个细节是它们没有身份。值对象之所以重要,是因为它们能告诉你关于另一个对象的信息。Person 实体有多高? InsurancePolicy实体有多少年没有理赔了? Fruit实体的重量是多少?希望你能从这些例子中明白为什么拥有身份是没有意义的。
As you learn about value objects, the single most important detail to remember is that they have no identity. Value objects are important because they tell you something about another object. How tall is a Person entity? How many years have there been no claims on an InsurancePolicy entity? What is the weight of a Fruit entity? Hopefully you can discern from these examples why having an identity would make no sense.
由于它们没有身份标识,并且描述了领域中的其他概念,因此通常你会先发现实体,然后再从中识别出与它们相关的值对象的类型。通常,值对象需要实体的上下文才能与其相关。
Because they have no identity and describe other concepts in the domain, normally you will uncover entities first, and from there realize the types of value objects that are relevant to them. Generally, value objects need the context of an entity to be relevant.
即使值对象没有身份,比较它们仍然是一项至关重要的操作。这可以通过基于属性或值的相等性来实现。
Comparing value objects is a crucial operation, though, even though they have no identity. This is enabled through attribute- or value-based equality.
如果实体具有相同的 ID,则认为它们相等。相反,如果值对象具有相同的值,则认为它们相等。
Entities are considered equal if they have the same ID. Conversely, value objects are considered equal if they have the same value.
如果两个Currency值对象代表相同金额,则无论每个变量指向的是同一个对象还是不同的对象,它们都被视为相等。同样,如果两个Temperature值对象代表 30 摄氏度,情况也是如此。清单 15-4 演示了一个Meters值对象,它用于个人健身系统的领域模型,用于追踪已覆盖的距离。清单 15-5 随后展示了测试用例,演示了其基于属性的相等性。
If two Currency value objects represent the same amount of money, they are considered equal, regardless of whether each variable points to the same object or a different one. Likewise, if two Temperature value objects represent 30 degrees Celsius, the same applies. Listing 15-4 illustrates a Meters value object used in the domain model of a personal fitness system that tracks how much distance has been covered. Listing 15-5 then shows test cases demonstrating its attribute-based equality.
清单 15-4 展示了Meters值对象如何重写Equals()来实现基于属性的相等性。默认情况下,如果两个对象指向同一个对象引用或指针,C#(以及其他语言,例如 Java)会认为它们相等。对于值对象,你不必关心两个引用是否指向同一个对象,你关心的是它们是否表示相同的领域相关值。在清单 15-4 中,你可以看到Equals()的实现通过返回 true 来履行这一义务,如果两个对象表示的距离(以米为单位,精确到小数点后两位,精度会根据领域规则和上下文而变化),则表示它们相等。
Listing 15-4 shows how the Meters value object overrides Equals() to implement attribute-based equality. By default, C# (and other languages like Java) will consider two objects to be equal if they point to the same object reference or pointer. With a value object, you don’t care if the two references point to the same object, you care about whether they represent the same domain-relevant value. In Listing 15-4, you can see the implementation of Equals() fulfils this obligation by returning true, indicating the two objects are equal, if they represent the same distance in meters to two decimal places (precision will vary based on domain rules and context).
清单 15-4 仅显示了演示基于属性的相等性所需的最少代码。为了在 C# 中获得完全的相等性支持,实际上需要重写编译器和运行时针对各种场景所需的一些其他方法。在所有值对象中实现所有这些方法可能会有点繁琐,尤其是在没有必要的情况下。如清单 15-6 所示,可以使用基类来避免不必要的繁琐和重复。
Listing 15-4 shows only the minimum amount of code necessary to demonstrate attribute-based equality. To get full equality support in C#, you actually need to override a few other methods that the compiler and run time require for various scenarios. Implementing all these methods in all your value objects can get a bit tiresome, especially when it is unnecessary. As shown in Listing 15-6, you can employ a base class to prevent unnecessary boredom and repetition.
为了确保您的类支持 C# 中的所有原生比较操作,您需要实现Equals()、GetHashCode()以及运算符重载==和!=。使用清单 15-6 所示的基类可以为您完成所有这些工作,从而使您的工作变得轻松。剩下您要做的就是在每个子类中实现GetAttributesToIncludeInEqualityCheck(),如清单 15-7 所示。如果您准备添加另一层复杂性,您可以研究基于反射的替代方案。
To ensure that your classes support all the native comparison operations in C#, you need to implement Equals(), GetHashCode(), and the operator overloads == and !=. Using a base class like the one shown in Listing 15-6 makes your life easy by doing all this for you. All that’s left for you to do is implement GetAttributesToIncludeInEqualityCheck() in each sub class, as shown in Listing 15-7. If you are prepared to add another layer of complexity, you can investigate reflection-based alternatives.
示例 15-7 展示了具有相同语义和行为的Meters的另一种实现。此外,它还支持==和!=。相比于重复应用ValueObject基类现在管理的样板代码,这是一个令人满意的改进。
Listing 15-7 shows an alternative implementation of Meters with the same semantics and behavior. In addition it also supports == and !=. This is a satisfying improvement over having to repeatedly apply the boilerplate that the ValueObject base class now manages.
值对象应尽可能地暴露富有表现力的面向领域行为并封装状态。清单 15-4 演示了这一点,其中Meters值对象暴露了以下行为:ToFeet()、ToKilometers()、Add()和IsLongerThan()。重要的是,还要注意它如何封装其DistanceInMeters状态。一般来说,所有原始值默认都应该是私有的或受保护的。只有当你有非常充分的理由时,才应该打破封装并将其公开。但首先考虑向值对象添加一个提供所需功能的方法。
As much as possible your value objects should expose expressive domain-oriented behavior and encapsulate state. This was demonstrated with Listing 15-4 where the Meters value object exposed the behaviors: ToFeet(), ToKilometers(), Add(), and IsLongerThan(). Importantly, note also how it encapsulated its DistanceInMeters state. As a general rule, all primitive values should be private or protected by default. Only when you have a very good reason should you break encapsulation and make them public. But first consider adding a method to the value object that provides the required functionality.
正如本书反复提到的,关注行为以创建行为丰富的领域模型是创建和演进领域模型的最关键方面之一,这样才能使领域概念清晰明确。这同样适用于值对象以及本书后面介绍的其他类型的领域对象。
As you will see repeated throughout this book. Focusing on behavior to create behavior-rich domain models is one of the most crucial aspects of creating and evolving a domain model, so that domain concepts are explicit. This applies equally to value objects and other types of domain objects that are introduced later in the book.
值对象是一个描述性概念,通常用数量来描述某物,它通常会内聚地封装测量值和测量单位。您已经在Height值对象中看到了这一点,它封装了尺寸和测量单位。同样,您也在Money值对象中看到了这一点,它封装了金额和货币。
As a descriptive concept, usually describing something with a quantity, value objects often cohesively encapsulate the value of measurement and the unit of measurement. You’ve seen this already with the Height value object, which encapsulates the size and the unit of measurement. Similarly, you also saw this with the Money value object, which encapsulates both the amount and the currency.
然而,内聚并不总是意味着封装一个值和度量单位。它可以是任意数量的字段。例如,一个Color值对象可能具有Red、Green和Blue属性。
It’s not always the case that being cohesive means encapsulating a value and unit of measurement, though. It could be any number of fields. For instance, a Color value object may have a Red, a Green, and a Blue property.
值对象一旦创建,就永远无法更改。相反,任何更改其值的尝试都应导致创建一个具有所需值的全新实例。这是因为不变性通常更容易推理,并且危险的副作用更少。
Once created, a value object can never be changed. Instead, any attempts to change its values should result in the creation of an entirely new instance with the desired values. This is because immutability is usually easier to reason about, with fewer dangerous side effects.
.NET 的DateTime类是不变性的一个经典示例,通常也是值对象的一个很好的榜样。清单 15-8 中的单元测试演示了如何调用任何看似改变对象状态的操作(包括AddMonths()和AddYears() ),实际上会返回一个全新的DateTime对象。
A classic example of immutability, and generally a good role model for value objects, is .NET’s DateTime class. The unit tests in Listing 15-8 exemplify how calling any operations that appear to mutate the object’s state, including AddMonths() and AddYears(), actually return a completely new DateTime object.
自己实现不可变性很容易。首先,你要决定如何暴露状态。一种选择是使用只读实例变量,如清单 15-9 所示。另一种方法是使用在构造函数中设置且永远不会改变的属性,如前面的清单所示。这两种方法都很常见,通常只是个人偏好或项目约定的问题。不过,有时框架可能会强迫你做出选择。
Implementing immutability yourself is easy. First, you decide how you are going to expose state. One option is to have readonly instance variables, as shown in Listing 15-9. The other approach is to have properties that are set in the constructor and never altered, as previous listings demonstrated. Both approaches are common, and it’s usually just a matter of personal preference or project conventions. Sometimes, though, frameworks may force you into one choice or the other.
除了状态本身之外,您还可以公开描述性方法名称,以面向域的术语表达新值的创建。对于DateTime,这些方法包括前面讨论过的AddMonths()和AddYears();对于清单 15-9 中的Money值对象,这些方法是Add()和Subtract()。
In addition to the state itself, you expose descriptive method names that express the creation of the new value in domain-oriented terms. For DateTime, those methods include the previously discussed AddMonths() and AddYears(); for the Money value object in Listing 15-9, the methods are Add() and Subtract().
示例 15-9 中的Money值对象有两个听起来像是改变其状态的方法:Add()和Subtract()。然而,查看其实现却发现并非如此,因为这两个方法都返回一个具有更新值的新Money实例,而原始实例则完全保持不变。不可变性也支持可组合性,如下例所示。
The Money value object in Listing 15-9 has two methods that sound like they mutate its state: Add() and Subtract(). Looking at the implementation tells a different story, though, because both methods return a new instance of Money with the updated value, while the original instance is left completely unchanged. Being immutable also supports combinability, as the next example illustrates.
值通常以数字形式表示,因此在很多情况下,它们可以组合起来创建一个新的值。正如你在前面的例子中看到的,Money可以添加到Money中以创建一个新的金额。像这样的可组合性通常是值对象的一个定义性特征。所以,当你与领域专家讨论如何组合某个概念的两个实例时,这清楚地表明你可能需要将该概念建模为值对象。
Values are often represented numerically, so in a lot of cases, they can be combined to create a new value. As you saw in the previous example, Money can be added to Money to create a new amount. Combinability like this is a defining characteristic of value objects in general. So when you are with domain experts and they talk about combining two instances of a certain concept, this is a clear sign that you may need to model the concept as a value object.
通过表示值或数量,在大多数情况下,值对象可以使用加法、减法和乘法等运算进行组合,如前面的不变性示例所示。为了增强表达能力,您可以在许多编程语言中基于每个对象覆盖这些本机操作。清单 15-10 展示了如何更新Money值对象以支持 C# 中的加号+和减号-运算符。
Through representing a value or quantity, in most cases value objects can be combined using operations like addition, subtraction, and multiplication, as the previous immutability example demonstrated. For enhanced expressiveness, you can override these native operations, on a per-object basis, in many programming languages. Listing 15-10 shows how the Money value object can be updated to support the plus + and minus - operators in C#.
在 C# 中重写+和-运算符非常简单,如清单 15-10 所示。清单 15-11 中的单元测试演示了如何使用这些运算符将不可变的Money对象组合成新实例。
Overriding the + and - operators is light work in C#, as Listing 15-10 shows. You can see unit tests in Listing 15-11 that demonstrate using these operators to combine immutable Money objects into new instances.
值对象永远不应该处于无效状态。它们本身全权负责确保这一要求。实际上,这意味着在创建值对象实例时,如果参数不符合领域规则,构造函数应该抛出异常。例如,如果您在电子商务应用程序中 使用Money值对象来建模货币,则可能存在两个重要的领域规则:
Value objects should never be in an invalid state. They themselves are solely responsible for ensuring this requirement. In practice, this means that when you create an instance of a value object, the constructor should throw an exception if the arguments are not in accordance with domain rules. As an example, if you are modeling money with a Money value object in an e-commerce application, there may be two important domain rules:
这两条规则适用于领域中所有Money实例,绝不能违反。清单 15-12 展示了一个Money值对象,它在其构造函数中强制执行这些约束,确保它永远不会在无效状态下创建。
These two rules apply to all instances of Money in the domain and should never be violated. Listing 15-12 shows a Money value object that enforces these constraints in its constructor, ensuring it can never be created in an invalid state.
Money构造函数中发生的第一件事是调用Validate(),它强制执行值对象重要的自验证特性。从实现角度来看,你无需付出太多努力。如示例 15-12 所示,在很多情况下,你只需要检查值对象的状态是否在允许的范围内。如果不在,只需抛出一个描述性异常并尽快中止构造。
The first thing that happens in Money’s constructor is the call to Validate(), which enforces the important self-validating characteristic of value objects. Implementation-wise, you don’t have to go to a lot of effort. As Listing 15-12 shows, in many cases, all you need to check is that the state of the value object is within the allowable range. If it’s not, you just throw a descriptive exception and abort construction as soon as possible.
不过,根据您的偏好,确实存在其他用于强制验证的编码模式。每种模式的灵活性和表现力会根据您的具体情况而有所不同,因此了解它们是值得的。第一种替代模式是在工厂方法内部进行验证,如清单 15-13 所示。
Other coding patterns do exist for enforcing validation, though, depending on your preferences. Each pattern’s flexibility and expressiveness can vary based on your context, so it’s worthwhile knowing about them. The first alternative pattern is to validate inside a factory method, as shown in Listing 15-13.
注意清单 15-13 中Create()是static 的。这是一个工厂方法,用作Money构造函数中验证的替代方法。当可以根据上下文创建不同状态的值对象时,可能需要使用此模式。例如,在某些情况下,允许负数 money 可能没问题,但在其他情况下则不行。
Notice in Listing 15-13 how Create() is static. This is the factory method that is used as an alternative to validating in Money’s constructor. You may want to use this pattern when value objects can be created in different states based on the context. For example, in some scenarios, it may be fine to allow negative money, but not in others.
使用工厂方法确实意味着你可以完全绕过验证,直接用new关键字创建实例。所以请谨慎使用。如果你遇到不同的上下文需要不同的验证规则,您一定要查看本章后面对微型或微小类型的介绍。
Using factory methods does mean that you can bypass validation altogether and create an instance with the new keyword. So use it with caution. If you have a situation in which different contexts require different validation rules, you should definitely check out the introduction to micro or tiny types later in this chapter.
验证值对象的另一种模式是使用代码契约,它可以用更高的表达能力和流畅性来弥补额外的技术复杂性。您可以使用代码契约来补充前面提到的任何一种模式。清单 15-14 演示了如何在构造函数验证中使用代码契约。
Another pattern for validating value objects is to use code contracts, which trade off additional technical complexity with greater expressiveness and fluency. You can use code contracts to supplement either of the previous patterns mentioned. Listing 15-14 illustrates using code contracts with constructor validation.
如果您确实倾向于代码契约,可以通过创建可重用的契约和辅助对象来最大限度地减少额外的复杂性。清单 15-15 展示了一些支持清单 15-14 中示例的基本代码契约实用程序。您可以自行构建这些辅助对象的库,也可以考虑使用现有的代码契约库,例如 Microsoft 官方提供的代码契约库(http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx)。
If you do prefer code contracts, you can minimize the additional complexity by creating reusable contracts and helper objects. Listing 15-15 shows a few basic code contract utilities that support the example in Listing 15-14. You are free to build a library of these helpers yourself or consider existing code contracts libraries such as the official Microsoft offering (http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx).
不变性、内聚性和可组合性是值对象的三个特性,使得它们在面向领域语言中易于测试。不变性使得无需使用模拟或验证副作用;内聚性允许对单个概念进行独立的完整测试;可组合性则允许你表达不同值之间的关系。
Immutability, cohesion, and combinability are three qualities of value objects that make them easy to test in expressive domain-oriented language. Immutability precludes the need to use mocks or verify side effects, cohesion allows single concepts to be fully tested in isolation, and combinability allows you to express the relationships between different values.
在本节中,您已经看到了许多单元测试的示例。但它们是在其他特性的背景下展示的,因此您可能没有花时间去体会它们的低摩擦性。清单 15-6 是另一个展示值对象可测试性的简短示例,演示了如何在没有模拟的情况下轻松地测试错误条件。
Throughout this section, you’ve already seen a number of examples of unit tests. But they were shown in the context of other characteristics, so you may not have taken the time to appreciate how low friction they are. Listing 15-6 is another short example showing the testability of value objects, demonstrating how error conditions can easily be tested without mocking.
通过观察清单 15-16 中的测试,很容易看出与测试其他对象(例如应用服务)相比,其摩擦程度更低。这主要是因为它缺乏副作用和可变性。你经常会听到函数式编程的支持者称赞这些特性。本质上,值对象本身就是一个函数式概念(或者至少非常接近)。
By observing the tests in Listing 15-16, it is easy to discern the lower levels of friction when compared to testing other objects like application services. This is mainly due to the lack of side effects and mutability. You’ll often hear proponents of functional programming lauding these characteristics. In essence, value objects themselves are a functional concept (or at least very close).
多年来,DDD 从业者积累了一些模式,旨在提升值对象的使用体验。这些模式主要是为了提升表达能力和清晰度,但也有一些其他细微的优势,例如可维护性。本节介绍三种基本模式,以便您可以立即上手使用,并开始思考您自己的模式。
DDD practitioners have built up a small collection of patterns over the years that improve the experience of working with value objects. Mostly, the benefits are aimed at improving expressiveness and clarity, but some have other slight benefits, including maintainability. This section presents three basic patterns so that you can immediately start to use them and start to think about patterns of your own, too.
使用静态工厂方法是一种流行的技术,它可以将对象构造的复杂性封装到一个更简单、更具表现力的接口背后。.NET 的TimeSpan类就是一个很好的例子,它包含FromDays()、FromHours()和FromMilliseconds()静态工厂方法。方法。这些替代方案比五个整数参数的构造函数更具表现力,歧义性更小,如示例 15-17 所示。
Using static factory methods is a popular technique for wrapping the complexities of object construction behind a simpler, more expressive interface. An excellent example of this is .NET’s TimeSpan class with its FromDays(), FromHours(), and FromMilliseconds() static factory methods. These alternatives are more expressive and less ambiguous than the five-integer-parameter constructor, as Listing 15-17 illustrates.
静态工厂方法是一种风格选择,您可以自由选择或忽略它。清单 15-18 展示了对之前Height值对象所做的更改,即为每个货币单位添加一个静态工厂方法。在这种情况下,代码可以说更具表现力,客户端更容易调用,也更易于维护,因为代码的客户端不再需要将自己与MeasurementUnit枚举耦合。
Static factory methods are a stylistic choice that you are free to choose and ignore as you wish. Listing 15-18 shows the change that is made to the previously shown Height value object by having a static factory method for each unit of currency. In this case, the code is arguably more expressive, easier for clients to call, and more maintainable because clients of the code no longer need to couple themselves to the MeasurementUnit enum.
避免使用原语可以减少导致错误的歧义,从而帮助你更明确地表达代码意图。本章前面已经讨论过这一点。一种名为“微类型”的模式进一步推进了这一原则,它用更具表现力的类型包装原本就表达能力强的类型。需要澄清的是,使用微类型时,被包装的类型不必是原语;它们可以是包装原语的明确概念。这可以说是有用的,因为它增加了上下文的清晰度,从而可以减少错误。
Avoiding primitives can help you be more explicit about the intent of your code by reducing error-causing ambiguity. This was already discussed previously in this chapter. A pattern called micro types takes this principle even further by wrapping already-expressive types with even more expressive types. To clarify, with micro types, the types being wrapped do not have to be primitives; they can already be explicit concepts that wrap primitives. This is useful, arguably, because it adds contextual clarity that can reduce errors.
微类型的示例首先以清单 15-19 所示的OvertimeCalculator域服务为例。请注意HoursWorked和ContractedHours类型的两个参数。
An example of micro types is presented initially with the OvertimeCalculator domain service shown in Listing 15-19. Note the two parameters of type HoursWorked and ContractedHours.
清单 15-19 中使用的HoursWorked和ContractedHours类型都是包装Hours值对象的微类型。OvertimeHours是返回的微类型,它也只是Hours的上下文包装器。所有四个类的定义如清单 15-20 所示。
Both the HoursWorked and ContractedHours types used in Listing 15-19 are micro types that wrap an Hours value object. OvertimeHours is the return micro type, which is also just a contextual wrapper for Hours. All four class definitions are shown in Listing 15-20.
从清单 15-20 和 15-21 中可以看出,HoursWorked和ContractedHours没有添加任何额外的行为或状态。对于OvertimeCalculator.Calculate()来说,接受两个Hours也同样简单。 实例。但这样做强调了参数名称的明确性,并依赖于调用者按正确的顺序提供两个Hours对象。使用微类型,可以让类型系统更加努力地工作,从而既能提高代码的明确性,又能避免人为错误。
As you can see from Listings 15-20 and 15-21, HoursWorked and ContractedHours add no additional behavior or state. It would be just as easy for OvertimeCalculator.Calculate() to accept two Hours instances. But doing so emphasizes making the parameter names explicit and relying on callers to supply the two Hours objects in the correct order. Using micro types, you make the type system work harder for you so that it both increases the explicitness of your code and prevents human error.
使用微类型远非业界最佳实践。事实上,人们对它的看法颇有分歧。一些人认为微类型是更清晰、更易于组合的代码的先决条件,但另一些人则认为微类型包含太多烦人的间接层。是否使用微类型模式取决于你自己。
Using micro types is far from an industry best practice. In fact, it’s quite divisive. Some claim micro types are a precursor to clearer, more composable code, but for others, micro types are too many layers of annoying indirection. It’s up to you to decide if you want to use the micro types pattern.
一些 DDD 从业者认为,永远不应该使用值对象集合。其理由是原始集合无法正确表达领域概念。也有人认为,使用值对象集合通常意味着需要使用某种形式的标识来挑选出特定的项,这显然违反了“值对象没有标识”的原则。不过,这并非社区中普遍适用的做法,但你应该有充分的理由避免这样做。
Some DDD practitioners feel that you should never have a collection of value objects. The rationale for this is that primitive collections do not properly express domain concepts. It is also argued that having a collection of value objects often means that you need to pick out specific items using some form of identity, which is clearly a violation of value objects having no identity. Again, though, this is not a universally applied practice within the community, but it is one that you should have a good justification for eschewing.
一个通过公开值对象集合来强调缺乏清晰度的例子是清单 15-22 中所示的Customer实体,它有多个PhoneNumber值对象。
An example that highlights the lack of clarity through exposing a value object collection is the Customer entity presented in Listing 15-22, which has multiple PhoneNumber value objects.
PhoneNumbers集合中包含多个PhoneNumbers。为什么?它们可能是住宅电话号码和手机号码吗?也可能是紧急联系人或工作电话号码。使用集合的话,这些信息不太明确。但在这个领域,PhoneNumbers是根据业务需求构建的。每个客户都必须提供住宅电话号码、手机号码和工作电话号码。
There are multiple PhoneNumbers that are part of the PhoneNumbers collection. Why? Could they be home and cell phone? Perhaps there’s an emergency contact or work number in there, too. It’s just not clear by using a collection. But in this domain, PhoneNumbers are structured according to business requirements. Each customer must supply a home, mobile, and work number.
清单 15-23 展示了一种更真实的领域建模方法,它清晰地使用结构化的方式对住宅、手机和工作电话号码等重要的领域概念进行建模。它还可以轻松更改这些号码,而无需进行 ID 查找或恶意攻击。
A more honest way of modeling the domain is shown in Listing 15-23, which clearly uses structure to model the important domain concepts of a home, mobile, and work phone number. It also makes it easy to change any of these numbers without requiring an ID lookup or a nasty hack.
在清单 15-23 中,修改后的Customer实体现在具有由PhoneBook值对象表示的PhoneNumbers。现在无需任何类型的 ID 查找即可访问每种类型的电话号码(家庭、工作和手机)。这一点很重要,因为正如反复提到的,值对象没有标识。
In Listing 15-23, the modified Customer entity now has its PhoneNumbers represented by a PhoneBook value object. Each type of phone number—home, work, and mobile—can now be accessed without requiring any type of ID look-up. This is important because, as has been repeatedly mentioned, value objects do not have an identity.
与示例 15-22 相比,示例 15-23 最重要的是意图和领域概念更加清晰。客户实体拥有家庭、工作和手机号码。这个重要的领域结构被编码成类型,并由类型系统强制执行。
Most importantly with Listing 15-23 compared to Listing 15-22 is that the intent and domain concepts are much clearer. Customer entities have a home, work, and mobile number. This important domain structure is encoded into types and enforced by the type system.
可以说,处理值对象最棘手的部分是持久化它们。对于像 RavenDB 和 EventStore 这样的面向文档的数据存储来说,这个问题比较小;使用这些技术,通常可以将值对象和实体存储在同一个文档中。对于 SQL 数据库,然而,规范化的传统根深蒂固,导致实现方面存在更多可变性。因此,本章的其余部分主要关注基于 SQL 的用例。
Arguably, the trickiest aspect of dealing with value objects is persisting them. With document-oriented data stores like RavenDB and EventStore, this is a lesser problem; with these technologies, it’s usually feasible to store the value object and the entity in the same document. With SQL databases, however, there is a strong tradition of normalization, which leads to more variability concerning implementation. Therefore, the remainder of this chapter mostly focuses on SQL-based use cases.
许多 NoSQL 数据库使用数据非规范化,这与 SQL 数据库规范化的严格惯例形成鲜明对比。NoSQL 对 DDD 有益,因为整个实体(有时是整个聚合)可以建模为单个文档。在面向文档的建模中,与连接表、规范化数据和通过 ORM 进行延迟加载相关的问题根本不存在。在值对象的上下文中,这仅仅意味着它们与实体一起存储。例如,清单 15-24 中所示的Customer实体和Name值对象可以存储为清单 15-25 中所示的单个非规范化的 JavaScript 对象表示法(JSON)文档。
Many NoSQL databases use data denormalization, which highly contrasts with the strong convention of normalizing SQL databases. NoSQL can be beneficial to DDD because entire entities—sometimes entire aggregates—can be modeled as single documents. Problems associated with joining tables, normalizing data, and lazy loading via ORMs just don’t exist with document-oriented modeling. In the context of value objects, this simply means that they are stored with the entity. For example, the Customer entity and Name value object shown in Listing 15-24 can be stored as the single, denormalized JavaScript Object Notation (JSON) document shown in Listing 15-25.
许多面向文档的 NoSQL 数据库将文档持久化为 JSON。清单 15-25 展示了如何将Customer及其Name值对象持久化为单个 JSON 文档。像 RavenDB 这样的文档数据库通常默认应用此约定,这意味着您只需创建一个适当结构化的对象模型。
A number of document-oriented NoSQL databases persist documents as JSON. Listing 15-25 shows how a Customer and its Name value object can be persisted in such a database as a single JSON document. Document databases like RavenDB usually apply this convention by default, meaning that you just have to create an appropriately structured object model.
虽然将值对象嵌入文档是一种常见的惯例,但它仍然是可选的。如有必要,出于性能考虑,您可以将值对象存储为单独的文档。
Although embedding value objects in documents is a common convention, it is still an optional one. You can store value objects as separate documents for performance reasons, if necessary.
在 SQL 数据库中持久化值对象有多种选择。您可以遵循标准 SQL 约定,在自己的表中对值对象进行规范化,也可以像清单 15-25 所示的面向文档的方法一样,对它们进行非规范化。本节分别展示了这两种主要方法的示例。值得指出的是,这些示例只是一个指南。每种方法都可能存在多种变化,因此您可以根据自己的需要自由定制和试验。
Persisting value objects in a SQL database comes with choices. You can follow standard SQL conventions and normalize your value objects in their own tables, or you can denormalize them in-situ akin to the document-oriented approach shown in Listing 15-25. This section shows an example of each of those two broad approaches. It’s worth pointing out that these examples are just a guide. There are many variations possible with each approach, so you are free to customize and experiment based on your own needs.
持久化值对象的一个常见模式是直接使用自定义表示来存储它们的值。当你不想在查询中使用额外的表和语句来连接它们时,这是一个很好的选择。
A common pattern for persisting value objects is to just store their value directly using a custom representation. This is a great choice when you don’t want to have extra tables and extra statements in your queries to join them.
DateTime是直接持久化值对象的良好模板,因为它们以文本形式存储在 SQL 数据库中。应用程序中没有单独的DateTime表来存储每个值。事实上, DateTime是一个非常常见的值,它有自己的数据库类型。即使没有明确的数据库类型,您也可以用这种方式建模自己的值对象。但是,要实现这一点,您需要创建自己的存储格式,并且可能需要教会您的框架和 ORM 如何使用它。
DateTime is a good template for persisting value objects directly because they are stored in a SQL database as a textual representation. There is no separate DateTime table that stores each value used in the application. In fact, DateTime is such a common value that it has its own database type. You can model your own value objects in this way, too, even without having an explicit database type. To achieve this, though, you need to create your own storage format, and you may need to teach your frameworks and ORMs how to use it.
为了能够在从持久层加载值对象时重新填充它们,您需要选择一种能够唯一标识每个可能值的格式。这样,值对象在保存到数据库时就可以以这种格式持久化,并在从数据库加载时以这种格式进行解析。
To be able to repopulate value objects when they are loaded from persistence, you are required to choose a format that uniquely identifies each possible value. A value object can then be persisted in this format when it is saved to the database and parsed from this format when it is loaded from the database.
重写ToString()是生成值对象持久化格式的一种可能模式。由于这种格式唯一地描述了一个值对象,因此它也有利于调试。清单 15-26 展示了一个更新的Name值对象,它重写了ToString()以返回其所代表值的唯一描述,目的是以该格式持久化该对象。清单 15-27 展示了一些单元测试,演示了它是如何工作的。
Overriding ToString() is one possible pattern for generating a value object’s persistence format. Because this format uniquely describes a value object, it can also be good for debugging. Listing 15-26 shows an updated Name value object that overrides ToString() to return a unique description of the value it represents, with the intention of persisting the object in that format. Listing 15-27 then shows some unit tests demonstrating how it works.
创建持久性格式主要是为每个值创建唯一的表示。这是这种持久性方法工作的最低要求。在清单 15-26 中,重写了ToString()以生成人类可读的表示。在您的应用程序中,您也可以偏爱人类可读的格式,但有时您可能希望选择占用空间较小甚至具有其他特性的格式。在选择存储格式时,还值得记住的是,有时您可能需要根据值过滤 SQL 查询。
Creating a persistence format is chiefly about creating a unique representation for each value. This is the minimum requirement for this approach of persistence to work. In Listing 15-26, ToString() is overridden to produce a human-readable representation. In your applications, you can also favor human-readable, but sometimes you may want to prefer formats that have a smaller footprint or even another quality. When choosing a storage format, it’s also worth keeping in mind that at some point you may need to filter SQL queries based on the values.
清单 15-26 中所示的实现的另一个可选方面实际上是重写ToString()。这样做是为了获得持久性和调试增强的双重好处。但是,如果您想要单独的ToString()和持久性格式,您可以重载ToString()或创建另一个方法,也许是ToPersistenceFormat()。最重要的细节是确保无论哪种方法生成要持久化的值,都会在持久化时由您手工制作的 SQL 查询或您选择的 ORM 调用。
Another optional aspect of the implementation shown in Listing 15-26 is actually overriding ToString(). This was done for the dual benefit of persistence and debugging enhancements. But if you wanted a separate ToString() and persistence format, you could either overload ToString() or create another method, perhaps ToPersistenceFormat(). The most important detail is ensuring that whichever method generates the value to be persisted is called at the time of persistence by your handcrafted SQL queries or your choice of ORM.
此示例展示了如何使用 ORM 持久化值对象(最复杂的情况)。如果您使用手动编写的 SQL 查询进行持久化,则可以轻松地将值对象的自定义表示形式传递给INSERT或UPDATE查询。
This example shows you how to persist a value object using an ORM (the most complex case). If you are persisting with handcrafted SQL queries, it should be easy to pass the custom representation of your value object into the INSERT or UPDATE query.
要持久化你的值对象,最重要的一步是指示你的 ORM。在 NHibernate 中,你可以通过创建一个客户IUserType来实现这一点,如清单 15-28 所示。大多数 ORM 框架可能都有类似的功能。
To persist your value object, the most important step is instructing your ORM. In NHibernate, you can do this by creating a customer IUserType, as Listing 15-28 shows. Most ORM frameworks are likely to have a similar feature.
NHibernate 的IUserType 接口允许开发人员手动控制特定类型对象的持久化。在清单 15-28 中,NameValueObjectPersister实现了IUserType接口来控制Name值对象的持久化方式。如清单 15-28 所示,IUserType.NullSafeSet()接口用于插入自定义逻辑,以生成存储在数据库中的值。
NHibernate’s IUserType allows developers to manually take control of persisting specific types of objects. In Listing 15-28, NameValueObjectPersister implements the IUserType interface to control how Name value objects are persisted. As you can see in Listing 15-28, IUserType.NullSafeSet() is where your custom logic is inserted to generate the value that is stored in the database.
在告知 ORM 以自定义格式持久化值对象,或者手动插入值之后,剩下的任务就是通过将值对象解析为值对象来处理对象的加载。实现此操作有几种可能的方法。首先,您可以创建一个构造函数,该构造函数接受值对象的字符串表示形式。或者,您可以创建一个中间组件,该组件解析字符串并以常规方式构造实例。本示例使用后一种方法,以避免任何持久化或框架相关的问题影响值对象。
Having told your ORM to persist your value object in a custom format, or having manually inserted the value yourself, the remaining task is to handle loading of the object by parsing the representation back into a value object. There are a few possible approaches to implementing this. First, you could have a constructor that takes a string representation of the value object. Alternatively, you could have an intermediate component that parses the string and constructs an instance in the normal way. This example uses the latter approach to avoid any persistence or framework-related concerns creeping into the value object.
与持久化一样,关键细节是找到允许您注入自定义解析逻辑的框架钩子。对于 NHibernate,该钩子是之前引入的IUserType上的另一个方法。您可以在清单 15-29 中看到如何从其持久化表示中解析Name值对象。
As with persisting, the key detail is finding the framework hook that allows you to inject your custom parsing logic. With NHibernate, that hook is another method on the IUserType that was previously introduced. You can see how to parse a Name value object from its persistence representation in Listing 15-29.
IUserType.NullSafeGet()是一个低级 NHibernate 钩子,它允许您从持久化状态手动重建对象。在清单 15-29 中,NullSafeGet()的实现从正在加载的Name值对象的纯文本表示中提取FirstName和Surname,并使用它们创建一个新的Name实例。如上所述,无论您使用哪种 ORM,都可能有一个等效的钩子,它为您提供低级控制,用于以自定义格式存储值对象并再次解析它们。
IUserType.NullSafeGet() is the low-level NHibernate hook that allows you to manually reconstruct an object from its persisted state. In Listing 15-29, the implementation of NullSafeGet() extracts the FirstName and Surname from the plain-text representation of the Name value object that is being loaded and uses them to create a new Name instance. As mentioned, whichever ORM you are using, there is likely to be an equivalent hook that gives you low-level control for storing value objects in a custom format and parsing them back again.
您可以通过本章的示例代码了解NameValueObjectPersister的完整实现,其中包括在存储和加载示例数据之前对 NHibernate 进行完整配置的单元测试。图 15.1展示了运行其中一个测试后数据库中存储的内容。
You can see the full implementation of NameValueObjectPersister with this chapter’s sample code, including unit tests that fully configure NHibernate before storing and loading sample data. Figure 15.1 shows an example of what is stored in the database after running one of the tests.
图 15.1以自定义格式存储的值对象。
FIGURE 15.1 Value object stored in custom format.
非规范化是值对象事实上的持久化策略,但规范化是 SQL 的一项重要传统。有时公司标准甚至会强制执行后者。不过,在某些情况下,规范化仍然是最佳选择。性能和效率的考虑是常见的例子——尤其是在值对象具有较大的表示形式,而您不希望每次加载所属实体时都加载它的情况下。
Denormalization is the de facto persistence strategy for value objects, but normalization is a strong SQL tradition. Sometimes company standards may even enforce the latter. Normalization can still be the best choice in some situations, though. Performance and efficiency reasons would be common examples—especially if the value object has a large representation that you do not want to be loaded every time the owning entity is loaded.
在这个例子中,你将了解如何映射实体-值对象关系,其中每种类型都有自己的表,并通过外键连接两者。与上一个例子一样,我们将使用 NHibernate 来演示此场景,但无论你使用什么技术,通用模式都适用。
In this example, you see how to map an entity-value object relationship, where each type has its own table, and where a foreign key joins the two. As in the last example, NHibernate will be used to demonstrate this scenario, but the general pattern can likely be applied with whatever technology you are using.
为了让 NHibernate 将Name值对象存储在新表中,第一个调整是添加一个受保护的零参数构造函数,如清单 15-30 所示。
For NHibernate to store the Name value object in a new table, the first adjustment is to add a protected zero-argument constructor, as Listing 15-30 illustrates.
令人惊讶的是,您需要做的额外工作很少。使用 Fluent NHibernate 时,只需要配置一个Join()映射关系,如清单 15-31 所示。
Surprisingly, there is little additional work you need to do. When using Fluent NHibernate, you need only configure a Join() mapping relationship, as shown in Listing 15-31.
通过使用Join()映射指令,清单 15-31 中的代码告诉 Fluent NHibernate 创建一个单独的CustomerName数据库表来存储Name值对象。KeyColumn ()指令告诉 NHibernate 使用客户的 ID 作为CustomerName表中的外键。因此,当在运行时加载Name时, Customer实体可以通过提供自己的 ID 来找到其Name。无论您使用哪种技术,都可能存在一些约定、配置或钩子允许您实现类似的功能。
By using a Join() mapping instruction, the code in Listing 15-31 is telling Fluent NHibernate to create a separate CustomerName database table that stores Name value objects. The KeyColumn() instruction tells NHibernate to use the customer’s ID as a foreign key in the CustomerName table. So when loading a Name occurs at run time, a Customer entity can find its Name by supplying its own ID. Whichever technology you are using, there is likely to be some convention, configuration, or hook that allows you to achieve something similar.
图 15.2展示了运行上述配置时 NHibernate 创建的表。此外,您可以在图 15.3中看到持久数据是如何存储的——值得注意的是,客户的 ID 被用作外键。
Figure 15.2 illustrates the tables that NHibernate creates when you run the preceding configuration. Additionally, you can see in Figure 15.3 how persisted data is stored—notably, the Customer’s ID is being used as the foreign key.
图 15.2客户和姓名的单独表格。
FIGURE 15.2 Separate table for Customer and Name.
图 15.3用于连接客户和姓名的客户外键。
FIGURE 15.3 Customer foreign key used to join Customer and Name.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 16 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 16 download and individually named according to the names throughout the chapter.
与领域专家合作时,他们经常会提及一些在问题领域中具有固有身份的概念。例如,他们可能会谈论某个特定的客户或某项特定的体育赛事。这些概念被称为实体,与值对象不同,两个具有相似值的实体通常不能被视为相等。
As you work with domain experts, they often refer to concepts that have an inherent identity in the problem domain. For instance, they may talk about a specific customer or a specific sporting event. These concepts are known as entities, and unlike value objects, it is usually not acceptable for two entities that have similar values to be considered equal.
出于概念和技术原因,在你的领域中查找实体并对其进行明确建模非常重要。如果你理解了概念就是实体,就可以开始向领域专家探究相关细节,例如它的生命周期。从技术上讲,你还需要了解哪些概念是实体,因为它们的设计和实现存在不同的权衡和考虑因素,正如你将在本章后面看到的那样。
Finding entities in your domain and modeling them explicitly is important for conceptual and technical reasons. If you understand that a concept is an entity, you can start to probe domain experts for related details, such as its life cycle. Technically, you also want to understand which concepts are entities because distinct trade-offs and considerations apply to their design and implementation, as you will see later in this chapter.
理解实体本质上是具有唯一标识的概念,无疑是本章的主要内容。虽然您将看到基于领域驱动设计 (DDD) 社区多年完善的实现指南和建模模式,但务必注意,随着新见解和新技术的出现,其中许多细节可能会随着时间推移而发生变化。因此,只要您理解实体的概念角色,并了解一般的实现注意事项,您就始终能够至少将领域模型构建到正确的状态。
Understanding that entities are primarily concepts with a unique identity is unquestionably the main takeaway from this chapter. Although you will see implementation guidance and modeling patterns based on years of refinement in the Domain-Driven Design (DDD) community, it’s important to be aware that many of these details can change over time with new insights and the emergence of new technologies. So as long as you understand the conceptual role of entities, and you are aware of general implementation considerations, you will always be in a good position to at least get your domain model in the right shape.
本节从理论上阐述了实体是什么以及它们与其他类型的域对象有何不同。
This section offers a theoretical look into what entities are and how they differ from other types of domain objects.
在您正在建模的领域中查找实体的一个好机会是注意领域专家的说话方式。例如,假设您正在与一位假日和旅行领域专家讨论。领域专家可能会向您解释说,旅行者选择他们喜欢的酒店,然后在那里预订假期。领域专家还会告诉您,不可以给旅行者预订其他酒店,即使该酒店名称相同或具有其他相同的值。在这种情况下,领域专家含蓄地告诉您,酒店是一个实体,因为它们的身份和唯一性很重要。没有它们,就不可能区分一家酒店和其他酒店,旅行者也无法获得他们期望的预订。
A good opportunity to find entities in the domain you are modeling is to pay attention to how domain experts speak. As an example, imagine you are in discussions with a holiday and travel domain expert. The domain expert may explain to you that travelers choose a hotel they like and then book a holiday there. The domain expert also informs you that it is not acceptable for the traveler to be given a booking at a different hotel—even if it has the same name or has other values that are the same. In this situation, the domain expert is implicitly telling you that hotels are an entity, because their identity and uniqueness are important. Without them, it would not be possible to distinguish one hotel from other hotels, and travelers would not get the booking they expected.
另一条需要从领域专家那里获取的信息是实体所具有的身份类型。在许多情况下,身份本身就是一个重要的领域概念,可以通过明确建模来提升领域模型的表达能力。以酒店为例,它可能拥有一个真实存在的 ID,这使得许多不同的旅行社和应用程序能够共享关于它的数据,例如房源情况和用户评论。另一方面,酒店可能没有真实存在的唯一标识符,因此应用程序需要生成一个任意的标识符。如果不清楚,那么最好询问领域专家,了解实体在问题领域中是否具有唯一身份。
Another piece of information to try to extract from domain experts is the type of identity an entity has. In many cases, identity can be an important domain concept in itself and can be explicitly modeled to improve the expressiveness of your domain model. In the case of a hotel, it might actually have a real-world ID that enables many disparate travel agents and applications to share data about it—like availability and user reviews. On the other hand, a hotel may have no real-world unique identifier, so an application would need to generate an arbitrary one. If it’s not clear, then it’s in your best interests to ask the domain expert(s) if an entity has a unique identity in the problem domain.
除了身份之外,另一个与其密切相关的迹象是,它表明你拥有一个实体:连续性。领域专家可能会给你一些线索,例如“订单已被接受”、“订单已在付款后确认”以及“订单已由快递员完成”。这些线索表明,这个“事物”在领域中有一个生命周期。通常,为了在领域中拥有生命周期,需要一个身份,以便能够在生命周期的各个阶段找到并更新该“事物”。
Aside from identity, but closely related, is another tell-tale sign that you have an entity: continuity. Domain experts may give you clues like, “The order is accepted,” “The order is confirmed after payment,” and “The order is fulfilled by the courier.” These clues indicate that this “thing” has a life cycle in the domain. And usually, to have a life cycle in the domain, an identity is needed to allow the “thing” to be found and updated at various stages of its life cycle.
发现实体的过程可能随时发生;您不一定需要与领域专家坐下来,先确定系统中的所有实体。一些 DDD 从业者会先识别领域中发生的事件。然后,他们会与领域专家合作,了解每个事件涉及哪些实体。通常,您应该始终保持警惕。
Uncovering entities can occur at any time; it’s not necessarily the case that you sit down with domain experts and first identify all the entities in a system up front. Some DDD practitioners start by identifying the events that occur in the domain. And from there they collaborate with domain experts to understand which entities are involved with each event. In general, you should always be on the lookout.
诚然,区分实体与其他类型的领域对象(例如值对象)可能颇具挑战性。如前所述,实体的本质在于身份——关注的是“谁”而不是“什么”。但确定一个概念是实体还是值对象仍然很困难,尤其是因为实体和值对象依赖于上下文;一个概念在一个领域中肯定是实体,在另一个领域中可能毫无疑问是值对象。
Admittedly, it can be challenging to distinguish entities from other types of domain objects, like value objects. As mentioned previously, entities are fundamentally about identity—focusing on the “who” rather than the “what.” But it can still be difficult to ascertain whether a concept is an entity or a value object, especially because entities and value objects are context dependent; a concept that is definitely an entity in one domain could unequivocally be a value object in another domain.
一个常见的 DDD 上下文依赖示例是金钱。在一个银行应用程序中,客户可能会在她的银行账户中存入 100 美元。当她将来某个时间点取出这 100 美元时,她收到的钞票或硬币可能与她存入的钞票或硬币不同。然而,这种差异并不重要,因为钱的身份并不重要;客户只关心钱的价值。因此,在这个领域,金钱无疑是一个值对象。但在另一个领域,例如涉及货币制造或可追溯性的领域,单张钞票或硬币的身份实际上可能是一个重要的领域概念。因此,每张钱都应该是一个具有唯一标识符的实体。
A common DDD example of context dependence is money. In a banking application, a customer might put $100 in her bank account. When she withdraws her $100 at some point in the future, she may receive different bank notes or coins than the ones she deposited. This difference is irrelevant, though, because the identity of the money is not important; the customer only cares about the value of the money. So in this domain, money is without a doubt a value object. But in another domain, perhaps involving the manufacture or traceability of money, the identity of individual notes or coins may actually be an important domain concept. So each piece of money would be an entity with a unique identifier.
大多数实体都具有相似的特征,因此您应该注意一些基本的设计和实现注意事项,包括分配标识、应用验证和委托行为。在本节中,您将看到针对每个基本设计注意事项的指导和示例,以便在发现域中的实体后,您可以开始在模型中实现它们。
Most entities have similar characteristics, so there are some fundamental design and implementation considerations that you should be aware of, including assigning identity, applying validation, and delegating behavior. In this section, you will see guidance and examples for each of the fundamental design considerations so that once you have uncovered entities in your domain, you can begin implementing them in your model.
有时,实体的身份由自然域标识决定,而有时可能没有预先存在的自然标识符。对于前者,您有责任与领域专家合作来揭示自然身份;而对于后者,您需要在应用程序中生成任意身份,这可能需要借助数据存储区的帮助。
Sometimes an entity’s identity is determined by a natural domain identity, whereas other times there may be no preexisting natural identifier. With the former, it’s your responsibility to collaborate with domain experts to reveal the natural identity, whereas for the latter, you need to generate an arbitrary identity in your application, potentially with help from your datastore.
在尝试确定实体的身份时,首先应考虑该实体是否已在问题域中具有唯一标识符。这些标识符称为自然键。以下是一些示例:
When trying to ascertain what an entity’s identity should be, you should first consider whether the entity already has a unique identifier in the problem domain. These are called natural keys. Here are a few examples:
你确实需要确保自然密钥永远不会改变。如果改变了,你的系统中可能会有很多指向旧身份的引用。更新所有这些引用也是一件麻烦事。最糟糕的是,您可能会忘记更新一些参考资料或犯下导致严重业务级问题的错误。
You do need to ensure that a natural key is never going to change. If it does, you may have lots of references in your system pointing to the old identity. At best, it’s a hassle to update all of them. At worst, you may forget to update some references or make mistakes that lead to significant business-level problems.
一旦确定了一个自然键,并且确信它确实是一个自然键,通常可以直接将其分配给一个实体。大多数情况下,只需要添加一个构造函数参数,如清单 16-1 所示。
Once you have identified a natural key, and you’re confident it definitely is a natural key, it’s usually straightforward to assign it to an entity. Most of the time, you just need to add a constructor parameter, as shown in Listing 16-1.
您可以在清单 16-1 中看到Book实体具有一个ISBN构造函数参数,该参数被设置为新创建实例的标识。您传入自然键,然后实体将假定该标识。
You can see in Listing 16-1 that the Book entity has an ISBN constructor parameter that is set as the identity of the newly created instance. You pass the natural key in, and the entity then assumes that identity.
使用自然键时需要注意的一个问题是,确保你的对象关系映射器 (ORM) 或数据访问技术已配置为允许使用它们。某些框架可能会覆盖你赋予实体的 ID,除非你明确告知框架由你自行管理该 ID。此问题也适用于任意生成的键,下文将对此进行介绍。
One issue to be cognizant of when using natural keys is ensuring that your object-relational mapper (ORM) or data access technology is configured to allow them. Some frameworks may override the ID you have given an entity unless you explicitly tell the framework that you are managing the ID yourself. This issue also applies to arbitrarily generated keys, which are covered next.
当问题域中没有唯一标识符时,您需要确定要使用哪种类型的 ID 以及如何生成它。常见的格式包括增量数字、全局唯一标识符 (GUID)(又称通用唯一标识符,UUID)和字符串。
When there is no unique identifier in the problem domain, you need to decide what kind of ID you are going to use and how you will generate it. Common formats include incremental numbers, globally unique identifiers (GUIDs) (aka universally unique identifiers, UUIDs), and strings.
数字通常占用空间最小,但它们本身就存在维护最新分配 ID 的全局计数器的挑战。相反,GUID 则很好,因为它们没有这个问题。您只需生成一个自动保证唯一的 GUID。然而,它在持久化时会占用更多存储空间。不过,额外的存储空间相对来说微不足道,因此对于许多应用程序来说,GUID 是默认方法。字符串通常用于自定义 ID 格式,例如哈希值、多个属性的合并,甚至基于时间戳的方法。
Numbers usually have the smallest footprint, but they inherently pose the challenge of maintaining a global counter of the last assigned ID. Conversely, GUIDs are good because they don’t have this problem. Instead, you just generate a GUID which is automatically guaranteed to be unique. However, it does then take up more storage space when it’s persisted. The additional storage is relatively insignificant, though, so for many applications GUIDs are the default approach. Strings tend to be used for custom ID formats such as hashes, amalgamations of multiple attributes, or even timestamp-based approaches.
清单 16-2 演示了如何使用增量数字,其中静态变量跟踪最后分配的 ID,工厂方法确保每个新实体获得其 ID 的下一个序列号。
Listing 16-2 demonstrates using incremental numbers, whereby a static variable keeps track of the last assigned ID and a factory method ensures that each new entity gets the next sequential number for its ID.
如果应用程序崩溃,那么像清单 16-2 中所示的lastId这样的静态变量就会丢失其值,这意味着应用程序重启后旧的 ID 可能会被重用。为了解决这个问题,你可能需要持久化计数器。但对于许多用例来说,这样做并非最佳选择,因为读取和更新的性能开销以及额外的复杂性。某些实现甚至需要在分布式或负载均衡环境中使用脆弱的锁定技术。这取决于你根据具体情况做出最佳判断,但如果生成 ID 是系统中复杂而脆弱的部分,那么 GUID 或许是一种让你的团队工作更轻松的简单方法。
If an application crashes, then a static variable like lastId shown in Listing 16-2 loses its value, meaning that old IDs are likely to be reused when the application restarts. To remedy this, you likely need to persist the counter. But doing so is suboptimal for many use cases because of the performance overhead of reading and updating combined with the additional complexity. Some implementations even require fragile locking techniques in distributed or load-balanced environments. It comes down to your best judgment on a case-by-case basis, but if generating IDs is a complex and fragile part of your system, then perhaps GUIDs are an easy way to make life much easier for your team.
与维护全局计数器相比,使用 GUID 可以大大简化操作。在许多情况下,性能、复杂性和同步问题完全消失,如清单 16-3 所示。
Using GUIDs can be a massive simplification over maintaining a global counter. In many cases, the performance, complexity, and synchronization problems completely go away, as Listing 16-3 shows.
GUID 保证唯一,因此无论在何处调用GUID.NewGuid(),它始终都会生成一个唯一标识符。因此,VehicleFactory.CreateVehicle()始终会创建具有唯一 ID 的车辆——即使在负载均衡环境中的多台服务器上同时调用相同的方法也是如此。这说明了为什么在生成自己的 ID 时应该优先使用 GUID。
GUIDs are guaranteed to be unique, so no matter where the call to GUID.NewGuid(), it always produces a unique identifier. Therefore, VehicleFactory.CreateVehicle() always creates a Vehicle with a unique ID—even if the same method was invoked at the exact time on multiple servers in a load-balanced environment. This demonstrates why you should often favor GUIDs if you’re generating your own IDs.
当浏览器中的逻辑需要创建实体并回发到多个后端应用程序编程接口 (API) 时,使用 GUID 会特别有用。如果没有 ID,后端服务就无法知道您正在向每个 API 发送同一实体的信息。您可以使用 JavaScript 在客户端创建 GUID 来解决这个问题。图 16.1演示了这个过程。
Using GUIDs can be especially useful when you have logic in the browser that needs to create an entity and needs to post back to multiple back-end application programming interfaces (APIs). Without an ID, there’s no way for the back-end services to know you are posting information about the same entity to each of them. You can solve this problem by creating a GUID on the client using JavaScript. Figure 16.1 illustrates this process.
图 16.1创建客户端 GUID 并发布到多个后端服务。
FIGURE 16.1 Creating a client-side GUID and posting to multiple back-end services.
在“DDD/CQRS”(命令查询职责分离)邮件列表中,关于客户端 ID 生成的讨论非常长。如果您正在考虑这种方法,或者想了解更多信息,那么这篇文章绝对值得一读 ( https://groups.google.com/forum/#!msg/dddcqrs/xYfmh2WwHKk/XW7eauXcKkcJ )。
There was a long discussion on the “DDD/CQRS” (Command Query Responsibility Segregation) mailing list about client-side ID generation. It’s definitely worth a read if you are considering this approach or want to learn more about it (https://groups.google.com/forum/#!msg/dddcqrs/xYfmh2WwHKk/XW7eauXcKkcJ).
使用字符串为您提供了创建自定义 ID 格式的机会。您可以使用多种策略来创建格式。一个例子是在 ID 中包含一些实体的状态以方便诊断。清单 16-4 展示了一个简化的示例,其中HolidayBooking实体的 ID 是一个字符串,其中包含预订该实体的旅行者的 ID、预订的开始和结束日期以及预订确认的时间戳。
Using strings is an opportunity for you to create your own custom ID format. There are many possible strategies that you can use to create formats. One example is including some of the entity’s state in the ID for diagnostic benefits. Listing 16-4 shows a simplified example of this, in which a HolidayBooking entity’s ID is a string containing the ID of the traveler who booked it, the start and end dates of the booking, and a timestamp of when the booking was confirmed.
清单 16-4 中的GenerateId()在HotelBooking实体的构造函数中被调用,以使用自定义格式生成唯一 ID。这是一个简单的例子,但它仍然与实际应用程序中使用的实现类似。
GenerateId() in Listing 16-4 is called in the constructor of the HotelBooking entity to generate a unique ID using a custom format. This is a trivial example, but it is still similar to implementations used in real applications.
与自然键类似,如果您希望将实体的值包含在其 ID 中以用于诊断目的,则这些值可能不应更改。因此,在创建自定义 ID 格式时,务必格外小心,并充分了解领域特征。
Similar to natural keys, if you want to include an entity’s values in its ID for diagnostic purposes, the values probably should not change. So it is worth being extra careful and having a good understanding of domain characteristics when creating a custom ID format.
将 ID 生成委托给数据存储通常足够简单且安全。大多数数据库,从 MS SQL Server 等 SQL 数据库到 RavenDB 等文档数据库,都原生支持 ID 生成。
It’s often easy enough and safe enough to delegate ID generation to your datastore. Most databases, ranging from SQL databases like MS SQL Server to document databases like RavenDB, natively support ID generation.
创建数据存储区生成的 ID 通常遵循类似的模式,即将新创建的实体传递到所选的数据访问库中。成功完成下一个事务后,您的实体将获得其 ID 集。清单 16-5 包含一个测试用例,使用由 SQL Server 支持的 NHibernate 演示了此行为。
Creating datastore-generated IDs often follows the similar pattern of passing your newly created entity into your chosen data access library. Upon successful completion of the next transaction, your entity then has its ID set. Listing 16-5 contains a test case demonstrating this behavior using NHibernate backed by SQL Server.
在清单 16-5 中,创建了两个没有 ID 的IdTestEntity实例。您可以看到,最初的两个断言验证了这一点。然后,在 NHibernate 事务中,两个实体都被保存(这相当于在 NHibernate 事务提交后被持久化)。最后两个断言验证了 ID 确实按预期设置。您可以在本章的示例代码中看到所有代码,包括 NHibernate 配置和设置。
In Listing 16-5, two IdTestEntity instances are created without an ID. You can see that the initial two assertions verify this. Then, inside a NHibernate transaction, both entities are saved (which equates to being persisted once the NHibernate transaction is committed). The final two assertions verify that the IDs were indeed set as expected. You can see all the code, including the NHibernate configuration and setup, in this chapter’s sample code.
让实体专注于身份识别的职责非常重要,因为这可以防止它们变得臃肿——当它们将许多相关行为整合在一起时,很容易陷入这种陷阱。要实现这一目标,需要将相关行为委托给值对象和领域服务。实际上,您在上一章中已经看到了这样的例子,其中值对象是可组合、可比较和自我验证的(从而将逻辑排除在使用它们的实体之外)。同样,您将在第17章“领域服务”中看到,通常情况下,乍一看似乎属于实体的无状态领域操作实际上可以封装为领域服务。
Keeping entities focused on the responsibility of identity is important because it prevents them from becoming bloated—an easy trap to fall into when they pull together many related behaviors. Achieving this focus requires delegating related behavior to value objects and domain services. You actually saw examples of this in the previous chapter when value objects were combinable, comparable, and self-validating (thus keeping logic out of the entities that use them). Equally, you’ll see in Chapter 17 “Domain Services,” that there are often cases where stateless domain operations that at first appear to belong to entities can actually be encapsulated as domain services.
为了演示将行为从实体推送到值对象的好处,清单 16-6 展示了清单 16-4 中引入的HolidayBooking实体的更新版本。此版本使用通用语言(UL)实现了关键的领域策略。首先,它确保假期的第一晚在最后一晚之前。其次,它确保预订满足三晚的最低住宿时长。它将这两个要求都转移到了Stay值对象上。
To demonstrate the benefits of pushing behavior from entities into value objects, Listing 16-6 shows an updated version of the HolidayBooking entity previously introduced in Listing 16-4. This version implements key domain policies using the Ubiquitous Language (UL). First, it ensures that the first night of a holiday precedes the last night. Second, it ensures that the booking meets the minimum duration of three nights. It offloads both of these requirements to the Stay value object.
清单 16-4 演示了最初的HolidayBooking实体,该实体仅关注身份。可以很容易地直接在该类内部实现新行为,而不是在清单 16-6 所示的Stay值对象中实现。然而,将此逻辑直接放在实体内部会降低表达能力,因为会将与身份相关的逻辑与与住宿(假期预订适用的时间段)相关的逻辑混合在一起。
Listing 16-4 demonstrated the initial HolidayBooking entity that was focused solely on identity. It would have easily been possible to implement the new behavior directly inside that class, instead of in the Stay value object shown in Listing 16-6. However, putting this logic directly inside the entity would reduce expressiveness by mixing in logic related to identity with logic related to stays (the period of time for which the holiday booking applies to).
您可能认为Stay类很小,逻辑直接放在HolidayBooking中就没问题了。但想象一下预订的其他方面,例如转机或航班等额外服务。如果将所有这些职责都放在HolidayBooking实体中,领域概念就会变得模糊和混乱。每次向实体添加行为时,都值得考虑是否可以将其放入值对象中,以增强领域概念的清晰度。
You might think that Stay is quite a small class and it would be fine for the logic to go directly inside the HolidayBooking. But imagine other aspects of a booking, such as extras like transfers or flights. If you put all of those responsibilities inside the HolidayBooking entity it would obscure and intermingle domain concepts. Any time you add behavior to an entity, it’s worth your while to consider whether you can push it into a value object for enhanced domain clarity.
在将行为推送到值对象时,需要注意实体的一个方面是对象图的深度。在清单 16-6 中,Stay属性是公共的,它的FirstNight和LastNight属性也是公共的。这使得实体的消费者可以调用HotelBooking.Stay.FirstNight。在这种情况下,三层深度是合理的,但您应该注意要公开多少对象图,因为客户端会将自己与它耦合。在这种情况下,如果您想重构FirstNight和LastNight属性,可能会很困难,因为系统的其他部分与它们紧密耦合。根据具体情况,您必须在对象图的公开深度与对象图的公开行为深度之间进行权衡。
One aspect of your entities to be conscious of when pushing behavior into value objects is the depth of the object graph. In Listing 16-6, the Stay property is public and so are its FirstNight and LastNight properties. That makes it possible for consumers of the entity to call HotelBooking.Stay.FirstNight. Three levels deep is reasonable in this case, but you should be careful about how much of your object graph to expose, because clients will couple themselves to it. In this scenario, if you wanted to refactor the FirstNight and LastNight properties, it may be difficult because other parts of the system are tightly-coupled to them. On a case-by-case basis you’ll have to trade-off the depth at which your object graph is exposed against how far up the object graph you bring public behavior.
除了身份之外,实体的主要实现要求是确保它们能够自我验证并始终有效。这与值对象的自我验证特性类似,但由于实体具有生命周期,因此它通常更依赖于上下文。例如,FlightBooking实体可能被允许在等待期间修改其DepartureDate 值。航空公司确认后,验证规则将根据业务策略阻止对DepartureDate进行更改。清单 16-7 展示了FlightBooking实体的一个实现,演示了这些上下文相关的验证规则。
In addition to identity, a primary implementation requirement for entities is ensuring they are self-validating and always valid. This is similar to the self-validating nature of value objects, although it’s usually much more context dependent due to entities having a life cycle. As an example, a FlightBooking entity may be allowed to have its DepartureDate modified while it is awaiting confirmation by the airline. Once confirmed, however, the validation rules then preclude changes to the DepartureDate in line with business policy. Listing 16-7 shows an implementation of the FlightBooking entity demonstrating these context-dependent validation rules.
当使用更新后的出发日期调用Reschedule()时,清单 16-7 所示的FlightBooking实体会抛出RescheduleRejected异常。但如果预订尚未确认,则不会发生这种情况。因此,此验证依赖于上下文,因为它仅适用于特定场景。您可能会觉得这与“实体始终有效”的说法相矛盾。然而,对于实体而言,“始终有效”实际上意味着“始终在上下文中有效”。
When Reschedule() is invoked with an updated departure date, the FlightBooking entity shown in Listing 16-7 throws a RescheduleRejected exception. But it doesn’t do that if the booking has not yet been confirmed. So this validation is context dependent because it only applies in certain scenarios. You may feel that this contradicts the statement that “entities are always valid.” However, “always valid” really means “always contextually valid” in the case of entities.
您还可以在清单 16-7 中看到,每个构造函数参数都经过了验证。这是确保实体始终有效的另一个重要准则。它还有助于防止出现问题在您的域中传播,并使您的域处于不一致的状态。此示例使用特定的异常类型来实现构造函数验证,但如何实现验证由您决定。通常,最好是具有表现力的。
You can also see in Listing 16-7 that each constructor argument is validated. This is another important guideline for ensuring that entities are always valid. It will also help to prevent problems spreading through your domain and putting your domain into inconsistent states. This example uses specific exception types for implementing constructor validation, but it’s up to you how you want to implement validation. In general it’s best to be expressive.
实体还负责执行一种更基本的验证形式:不变量。不变量是关于实体的事实。它们要求某些属性的值必须在一定范围内,才能准确表示被建模的实体。
Entities are also responsible for enforcing a more fundamental form of validation: invariants. Invariants are facts about an entity. They mandate that the values of certain attributes must fall within a certain range to be an accurate representation of the entity being modeled.
在酒店的语境中,为了找到不变量,你可能会问自己:“是什么让酒店成为酒店?”或者“酒店之所以成为酒店,意味着什么?”在旅游和度假领域,酒店代表着一栋带有房间的建筑,旅行者可以预订房间作为度假的基地。如果一栋建筑没有房间,它就不可能成为酒店。清单 16-8 展示了如何强制执行“成为酒店必须有房间”这一不变量。
In the context of hotels, to find invariants, you might ask yourself, “What makes a hotel a hotel?” or “What does it mean for a hotel to be a hotel?” In a travel and holidays domain, a hotel represents a building with rooms that travelers can book as the base for their holiday. If a building doesn’t have rooms, there’s no way it can be a hotel. Listing 16-8 shows how the invariant of “being a hotel necessities having rooms” can be enforced.
强制执行不变量不必复杂,但理想情况下应该明确。示例 16-8 中的EnforceInvariants()就是一个示例。在Hotel开始构建之前,会触发此方法,并验证构造函数参数是否满足酒店的基本要求。在本例中,这意味着确保Hotel至少有一间客房。如果此条件不满足,则会显式抛出HotelsMustHaveRooms异常,这样就毫无疑问地确定了不变量的存在,并且无法绕过它。
Enforcing invariants doesn’t have to be complicated, although ideally it should be explicit. This is exemplified in Listing 16-8 with EnforceInvariants(). Before construction of the Hotel begins, this method is fired and validates that the constructor arguments satisfy the fundamental requirements of a hotel. In this case, that means ensuring that the Hotel has at least one room. Upon failure of this condition, an explicit HotelsMustHaveRooms exception is thrown, leaving no doubts about the invariant and no way to bypass it.
如上所述,验证和不变量在本质上和外观上相似。您可以通过比较示例 16-7 和示例 16-8 来证明这一点。然而,一个微妙但重要的区别是,示例 16-8 中的不变量始终适用,无论上下文如何。这种微妙的差异非常重要,因为在学习领域知识时,您需要能够区分上下文验证规则和不变量,以便准确地理解和建模您的问题领域。
As mentioned, validation and invariants are similar in nature and appearance. You can see evidence of this by comparing Listing 16-7 and Listing 16-8. However, the subtle, but significant, difference is that the invariant in Listing 16-8 always applies regardless of context. Subtle differences like this are important, because when you are learning about domains, you need to be able to distinguish contextual validation rules from invariants to accurately understand and model your problem domain.
在本章后面,您将看到可以使用规范将验证行为推到实体之外。您还将看到,当任何实体具有多个状态时,如果您想使用状态模式,则可能需要考虑显式地对它们进行建模。
You will see later in this chapter that it is possible to push validation behavior out of entities using specifications. You will also see that when any entity has a number of states, you might want to consider modeling them explicitly if you are tempted to use the state pattern.
许多 DDD 从业者普遍认为,实体应该以行为为导向。这意味着实体的接口应该公开表达领域行为的表达方法,而不是公开状态。更概括地说,这与面向对象编程的“告诉而不是询问”原则密切相关。
A common opinion that many DDD practitioners share is that entities should be behavior oriented. This means that an entity’s interface should expose expressive methods that communicate domain behaviors instead of exposing state. More generally, this is closely related to the OOP principle of “Tell Don’t Ask.”
使用 DDD 时,关注实体的行为非常重要,因为它能提升领域模型的表达能力。此外,通过封装实体的状态,该状态只能由封装它的实例操作。这意味着任何需要修改状态的行为都必须属于该实体。这是可取的,因为它可以防止您将属于实体的逻辑放在错误的位置。
Focusing on an entity’s behavior when using DDD is important because it makes your domain model more expressive. Also, by encapsulating an entity’s state, that state can only be operated on by the instance that encapsulates it. This means that any behavior that needs to modify the state has to belong to the entity. This is desirable because it prevents you from putting logic that belongs to an entity in the wrong place.
要实现以行为为中心的实体,您需要谨慎暴露 getter,并且对暴露 setter(使其公开)极其敏感。暴露 setter 意味着实体状态被系统其他部分更新的风险增加,而更新方式无法解释更新原因——隐藏领域概念或更新原因,如下一个示例所示。
To implement behavior-focused entities you need to be wary of exposing getters and extremely sensitive to exposing setters (making them public). Exposing setters means there is an increased risk that an entity’s state will be updated by other parts of the system in a way that offers no explanation of why it is being updated—hiding the domain concept or the reason for the update, as the next example demonstrates.
如果你正在建模一个涉及跟踪足球比赛结果的问题域,你可能会想实现一个类似于清单 16-9 的SoccerCupMatch实体,其中暴露了该实体的所有状态。然而,你应该仔细考虑如何实现这样的面向状态的设计。
If you were modeling a problem domain that involved tracking the results of soccer matches, you might be tempted to implement a SoccerCupMatch entity similar to Listing 16-9, in which all the entity’s state is exposed. However, you should think carefully about implementing a state-oriented design like this.
如果将实体的状态公开供公众访问和修改,就像清单 16-9 中的SoccerCupMatch那样,那么就留下了这样的可能性:属于该实体的行为可能位于领域中不太明确的其他地方。例如,足球有一条有趣的规则,称为客场进球规则。两支球队在杯赛中交手两次,每队在主场各一次。如果总比分平局,那么在对手主场进球最多(客场进球)的球队获胜。例如,如果第一场比赛在 A 队主场以 1-0 获胜,第二场比赛在 B 队主场以 2-1 获胜,则总比分是 2-2。但是,A 队进了 1 个客场球,B 队进 0 个客场球。因此 A 队将是获胜者。
If you expose an entity’s state for public access and modification, as the SoccerCupMatch in Listing 16-9 does, you leave open the possibility that behavior belonging to the entity can be located elsewhere in the domain that is less explicit. For instance, soccer has an interesting rule called the away goals rule. Two teams play each other twice in a cup match, once at the home stadium for each team. If the overall score is a draw then the team that scored the most goals at their opponent’s stadium (away goals) is the winner. For example, if the first match was 1–0 to team A at team A’s stadium, and the second match was 2–1 to team B at team B’s stadium, the overall score would be 2–2. However, team A scored 1 away goal and team B scored 0 away goals. So team A would be the winner.
如果查看清单 16-9 中修改后的SoccerCupMatch实体,您会发现它只暴露了其状态——比分。该实体的客户端可能会将比分视为 2-2,并错误地认为比赛是平局或 B 队获胜。此外,该实体可能有多个客户端,它们都以不同的方式实现了检查比赛获胜者的逻辑。这种不一致会导致领域模型出现奇怪且不正确的行为。相反,SoccerCupMatch实体应该封装其状态并暴露计算获胜者的行为,就像清单 16-10 中的更新版本一样。
If you look at the revised SoccerCupMatch entity in Listing 16-9, you can see that it only exposes its state—the score. Clients of this entity may see the score as 2–2 and erroneously behave like the match was a draw or that team B won. Potentially, there could be multiple clients of the entity that all implement logic for checking the winner of a match in different ways. Such inconsistencies would lead to strange and incorrect behavior in the domain model. Instead, the SoccerCupMatch entity should encapsulate its state and expose the behavior for calculating a winner as the updated version in Listing 16-10 does.
即使你不知道什么是足球,通过查看示例 16-10 中的SoccerCupMatch实现,你也能推断出客场进球规则的含义。同样重要的是,通过使状态私有化并仅暴露行为,SoccerCupMatch的客户端只能使用领域术语与SoccerCupMatch实例进行通信,而不能仅仅修改状态。这是朝着创建行为丰富、富有表现力的领域模型迈出的一大步。
Even if you didn’t know what soccer was, by looking at the SoccerCupMatch implementation in Listing 16-10, you would be able to infer the meaning of the away goals rule. Also as important, by making the state private and only exposing behavior, clients of SoccerCupMatch can only communicate with instances of SoccerCupMatch using domain terminology as opposed to just mutating state. This is a big step toward creating behavior-rich, expressive domain models.
即使出于最好的意图,有时你也会面临巨大的妥协,似乎别无选择地暴露 getter 和 setter。在本章后面,你将看到备忘录模式如何减轻暴露 getter 和 setter 的需要。
Even with the best of intentions, sometimes you are faced with big compromises in which you appear to have no choice to expose getters and setters. Later in this chapter, you see how the memento pattern can alleviate the need to expose getters and setters.
尽管捕捉精确的问题域行为是 DDD 的一项基本实践,但一个令人遗憾的后果是,DDD 的新手往往会对过多的行为进行建模。他们天真地误以为 DDD 就是对现实世界进行建模。因此,他们会尝试对一个实体的许多实际行为进行建模,而这些行为实际上与正在构建的软件应用程序无关。这种情况相当常见,而且完全可以理解,因为团队出于好意才会这么做。不幸的是,这会带来问题,因为它会增加领域模型中的概念数量和复杂性。如果代码库中某些部分似乎从未使用过,还会导致混乱。
Even though capturing precise problem domain behavior is a fundamental practice of DDD, an unfortunate consequence is that people new to DDD tend to model too much behavior. They innocently buy into the fallacy that DDD is about modeling the real world. Subsequently, they try to model many real-world behaviors of an entity that are not actually relevant to the software application being built. This is a fairly common occurrence, and a completely understandable one that arises out of a team’s best intentions. Unfortunately, it is problematic because it increases the number of concepts and complexity in a domain model. It also leads to confusion if there are parts of a codebase that appear to never be used.
一旦你明白了你的领域模型并非旨在成为实际问题领域的全面模型,你很可能就不会犯这个错误,或者即使犯了,你也会很快意识到并改正。但还有一个类似的问题有时更难察觉;有时实体是根据 UI 需求来塑造的。通常,最好的解决方案是将所有转换或 UI 相关逻辑添加到视图模型或数据传输对象 (DTO) 中,以避免污染实体的接口和职责。
Once you understand that your domain model is not intended to be a full-scale model of the actual problem domain, you probably won’t make this mistake, or if you do, you will quickly realize and correct yourself. But there is a similar problem that’s sometimes harder to detect; sometimes entities are shaped based on UI requirements. Often, the best solution is to add any translations or UI-related logic onto a view model or data transfer object (DTO) to avoid polluting an entity’s interface and responsibilities.
技术概念潜入实体是很常见的现象。常见的例子包括依赖注入属性、验证属性以及诸如延迟加载之类的 ORM 功能。同样,对许多人来说,这些被视为降低领域模型表达能力的污染物。通常情况下,最好尽可能避免它们。诚然,有时你确实会发现自己陷入困境,必须在对干净领域模型的渴望和以尽可能简单的方式完成任务的需求之间做出务实的权衡。在这种情况下,你可能不得不咬紧牙关,接受污染领域模型的痛苦,但不要轻易屈服。
It’s a familiar sight for technical concepts to creep into an entity. Common examples include dependency injection attributes, validation attributes, and ORM functionality like lazy loading. Again, to many, these are considered pollutants that decrease the expressiveness of a domain model. It’s often best to avoid them as much as possible. Admittedly, sometimes you really will find yourself in a tough situation in which you have to pragmatically balance out the desire for a clean domain model with the need to get things done in the simplest way possible. In such situations, you may have to grit your teeth and accept the pain of polluting your domain model, but don’t give in without a fight.
近年来,分布式系统已成为新常态。随之而来的是,领域模型,尤其是实体,出现了新的设计选择。然而,绝大多数 DDD 从业者强烈建议不要分布式实体。本质上,这意味着实体应该被限制在单个领域模型中的单个类中,并位于单个有界上下文中。如果考虑清单 16-11 中所示的典型Customer实体,则此指导更有意义。
In recent years distributed systems have become the new normal. Subsequently, new design choices have arisen for domain models and, in particular, entities. Overwhelmingly, however, most DDD practitioners strongly suggest not distributing entities. In essence, this means that an entity should be confined to a single class in a single domain model inside a single bounded context. This guidance makes more sense if you consider the typical Customer entity shown in Listing 16-11.
地址、个人信息、订单历史记录、支付详情和忠诚度信息都包含在清单 16-11 中的Customer实体中。在单体应用程序中,可能有一些理由来证明这种设计是合理的。但在分布式系统中,地址、订单、支付详情和忠诚度信息可能位于不同的有界上下文中。因此,要从持久层加载此实体,可能需要在不同的有界上下文中对多个数据库进行查询。正如您在第 11 章到第 13 章中所看到的,分布式组件之间的紧密耦合很容易受到可扩展性、弹性以及由于涉及网络而产生的其他问题的影响。
Addresses, personal details, order history, payment details, and loyalty are all present on the Customer entity in Listing 16-11. In a monolithic application, there may be some reason to justify this design. But in a distributed system, addresses, orders, payment details, and loyalty information may reside in different bounded contexts. So to load this entity from persistence, queries against multiple databases, in different bounded contexts, might be necessary. As you saw in Chapters 11 through 13, tight coupling between distributed components is highly susceptible to scalability, resiliency, and other problems arising from the fact that there is a network involved.
考虑到分布,清单 16-11 中的Customer实体实际上代表了许多存在于不同有界上下文中的不同概念。清单 16-12 说明了如何在考虑分布和有界上下文划分的情况下重构Customer实体的忠诚度、订单和付款方面。
With distribution in mind, the Customer entity in Listing 16-11 actually represents a number of different concepts that live in different bounded contexts. Listing 16-12 illustrates how the loyalty, orders, and payments aspects of the Customer entity can be remodeled with distribution and bounded context partitioning in mind.
清单 16-12 中的每个实体都包含从清单 16-11 中引入的臃肿的Customer实体中获取的大量功能。所有这些实体都有一个CustomerId,即使它们位于不同的有界上下文中,也可以将它们的信息组合起来。这种解决方案在分布式系统中可能会被证明问题更少。而且,作为额外的好处,有界上下文的加入可以更好地与问题域保持一致。
Each of the entities in Listing 16-12 contains a chunk of functionality taken from the bloated Customer entity previously introduced in Listing 16-11. All these entities have a CustomerId that enables the information from them to be combined, even though they reside in different bounded contexts. This solution is likely to prove significantly less problematic in a distributed system. And, as a bonus, the inclusion of bounded contexts better aligns with the problem domain.
甚至在 DDD 应用于分布式系统之前,将系统分解为有界上下文并分配职责的概念(例如清单 16-11 中的Customer实体)就一直是强烈推荐的。因此,本节中关于分布的指导实际上适用于任何具有多个有界上下文或可以从中受益的 DDD 实现。
Even before DDD was being applied to distributed systems, the notion of decomposing systems into bounded contexts and apportioning responsibilities, like those of the Customer entity in Listing 16-11, has always been a strong recommendation. Therefore, the guidance presented in this section around distribution actually applies to any DDD implementation that has, or can benefit from, multiple bounded contexts.
本节将介绍一些可以提升实体表现力和可维护性的原则和模式。不过,这些并非唯一的模式。事实上,本节的目的是向您展示,在实现实体时,如果已经先了解了上一节中的基本原理,那么仍然有发挥创造力的空间。
In this section you are introduced to a few principles and patterns that can improve the expressiveness and maintainability of your entities. These aren’t the only patterns, though. In fact, the aim of this section is to show you that there is room for creativity when implementing entities, assuming that the fundamentals in the previous section have been considered first.
规范是小型的、单一用途的类,类似于策略。使用规范进行验证和不变量的好处包括:通过将单个概念封装在其自己的类中来增强表达能力,以及提高不可变、无副作用逻辑的可测试性。使用规范也是将行为从实体中移出到其他类型对象中的一个例子,正如本章前面所建议的那样。
Specifications are small, single-purpose classes, similar to policies. The benefits of using specifications for validation and invariants include enhanced expressiveness through encapsulating a single concept in its own class and the increased testability of immutable, side-effect-free logic. Using specifications is also an example of pushing behavior out of entities and into other types of objects, as recommended earlier in the chapter.
清单 16-13 包含本章前面FlightBooking实体更新版本的摘录。此版本使用规范来帮助应用其重新安排业务规则。
Listing 16-13 contains an extract of an updated version of the FlightBooking entity from earlier in the chapter. This version uses a specification to help apply its rescheduling business rule.
基本上,评估是否可以接受新出发日期的逻辑已被卸载到一个名为NoDepartureReschedulingAfterBookingConfirmation的单独类中,该类完美地描述了业务规则。该类及其实现的通用ISpecification接口如清单 16-14 所示。
Basically, the logic for assessing whether the new departure date can be accepted has been offloaded into a separate class called NoDepartureReschedulingAfterBookingConfirmation, which perfectly describes the business rule. This class is shown in Listing 16-14 along with the generic ISpecification interface it implements.
使用此模式变体的规范只有一个方法IsSatisfiedBy(),如清单 16-14 所示。如果建模的业务规则不能应用于传入的对象,则IsSatisfiedBy()应该返回false。
A specification using this variation of the pattern has a single method, IsSatisfiedBy(), as shown in Listing 16-14. IsSatisfiedBy() should return false if the business rule being modeled cannot be applied to the passed-in object.
您可能想知道创建单独的规范和使用通用接口的开销是否真的值得。这当然值得商榷——既要有单独的规范,又要费心去实现接口。不过,此模式还有另一个关键方面受益于ISpecification抽象,并且可能让您相信它的实用性:规范的组合,如清单 16-15 所示。
You may be wondering if the overhead of creating separate specifications and using a generic interface is actually worth it. It’s certainly debatable—both having separate specifications and bothering to have the interface. There’s another key aspect of this pattern, though, that benefits from the ISpecification abstraction and might convince you of its usefulness: the composition of specifications, as demonstrated in Listing 16-15.
一些航空公司会为常旅客提供一些诱人的小福利,例如短期通知改期。如果要更新“确认后不改期政策”,使其不适用于特定客户,可以使用OrSpecification对其进行建模,如清单16-14所示。OrSpecification首先尝试一个规范,如果失败,则尝试另一个。考虑到这一点,清单 16-15 现在应该有意义了;OrSpecification首先尝试FrequentFlyersCanRescheduleAfterBookingConfirmation规范,只有失败后,它才会恢复到NoDepartureReschedulingAfterBookingConfirmation规范,这符合该领域的业务规则。
Some airlines provide frequent flyers with tasty little perks such as short-notice rescheduling. If you wanted to update the “no rescheduling after confirmation policy” to not apply to select customers, you could model it using an OrSpecification, as shown in Listing 16-14. An OrSpecification first tries one specification, and if that fails it tries the other one. With this in mind, Listing 16-15 should now make sense; the OrSpecification first tries the FrequentFlyersCanRescheduleAfterBookingConfirmation specification, and only if that fails does it revert to the NoDepartureReschedulingAfterBookingConfirmation specification, in line with the business rules of this domain.
您可以在清单 16-16 中看到OrSpecification的实现。
You can see the implementation of OrSpecification in Listing 16-16.
在ISpecification的基础上,您还可以用其他方式组合规范。例如,您可以创建一个AndSpecification,要求两个规范的计算结果都为true。此外,您还可以创建一个基类或应用构建器模式来流畅地组合规范,如清单 16-17 所示。
Building on the foundation of ISpecification, you can also compose specifications in other ways. For example, you can create an AndSpecification that requires both specifications to evaluate to true. In addition, you can create a base class or apply the builder pattern for fluently composing specifications, as suggested in Listing 16-17.
许多领域中的实体会自然地展现出不同的生命周期阶段或状态。在每个状态下,通常只有实体行为的子集适用。例如,在线外卖订单可能具有“厨房排队”、“准备中”、“烹饪中”和“外送”等状态。显然,当订单外送时,它不能被放入烤箱;或者,当订单在烤箱中时,它不能开始准备,因为准备工作已经完成了。
Many domains have entities that naturally exhibit different life cycle stages or states. In each state, usually only a subset of the entity’s behavior is applicable. For example, an online takeaway order may have the states “in kitchen queue,” “being prepared,” “being cooked,” and “out for delivery.” Obviously, when an order is out for delivery, it cannot be put in the oven, or when the order is in the oven, it cannot start being prepared because that has already happened.
由于实体的生命周期,有时人们会倾向于使用状态模式来建模实体。然而,一些 DDD 从业者强烈反对对实体随意使用状态模式。相反,替代方案是为每个状态指定显式的类。
Because of its life cycle, there is sometimes a temptation to model entities using the state pattern. However, some DDD practitioners strongly discourage liberal use of the state pattern for entities. Instead, the alternative is to just have explicit classes for each state.
为了使用状态模式将刚才描述的在线外卖订单场景实现为实体,清单 16-18 展示了OnlineTakeawayOrder实体以及每个状态的接口和一个示例状态的实现。
To implement the online takeaway order scenario just described as an entity using the state pattern, Listing 16-18 shows the OnlineTakeawayOrder entity along with the interface for each state and an implementation of one example state.
清单 16-18 强调了状态模式最大的诟病:它会导致大量的样板代码和未实现的方法。这些问题在InKitchenQueue中表现得尤为明显,它是这个简化场景中五种可能状态之一。每个状态都刻意只实现一个方法。对于在特定状态下无效的方法,状态实现会抛出ActionNotPermittedInState异常。虽然这个例子比较极端,因为每个状态实现只处理一个方法,但状态模式的许多实现实际上比这个例子更加杂乱和冗长。
Listing 16-18 accentuates the biggest criticisms of the state pattern: it can result in massive amounts of boilerplate code and unimplemented methods. These problems are made clear by InKitchenQueue, which is one of five possible states in this simplified scenario. Each implements only one method purposefully. For the methods that aren’t valid in a particular state, the state implementation throws an ActionNotPermittedInState exception. Although this example is extreme in that each state implementation handles only a single method, many implementations of the state pattern really are far noisier and more verbose than this.
与 DDD 更相关的是状态模式不太明确。通过允许不应调用的方法,状态模式没有明确地建模领域规则。如果您无法打包正在烘烤的订单,一些 DDD 从业者认为类型系统应该明确地强化这一点。您可以通过为每个状态设置一个单独的实体来实现这一理想,并且只有适用于该状态的操作才被建模为实体接口的一部分。清单 16-19 展示了将此理念应用于在线外卖场景的结果。
More relevant to DDD is the fact that the state pattern is less explicit. By allowing methods that should not be called, the state pattern does not explicitly model the rules of the domain. If you cannot package an order that is in the oven, some DDD practitioners argue that the type system should reinforce this explicitly. You reach this ideal by having a separate entity for each state, with only the applicable operations for the state modeled as part of the entity’s interface. Listing 16-19 shows the result of applying this philosophy to the online takeaway scenario.
清单 16-19 展示了两个新的实体,它们使得IOnlineTakeawayOrderState及其实现完全冗余。可以看到,噪音减少了,样板代码也完全消失了。然而,真正的好处是,领域规则现在在代码中更加清晰;不再存在单个OnlineTakeawayOrder实体在领域模型中传递,从而允许对其调用无效方法。相反,领域模型必须明确指定它需要操作哪个状态,从而产生类型系统强制执行的更具表现力的领域规则。
Listing 16-19 shows two new entities that make the IOnlineTakeawayOrderState and its implementations completely redundant. You can see that the noise has been cut down and the boilerplate is completely gone. However, the real benefit is that the rules of the domain are now much clearer in the code; no longer is there a single OnlineTakeawayOrder entity that is passed around the domain model allowing invalid methods to be called on it. Instead, the domain model must explicitly specify which state it needs to operate on, resulting in more expressive domain rules that the type system enforces.
如果您认同本章中提出的避免使用 getter 和 setter 有助于创建行为丰富的领域模型的指导,那么您实际上很可能会遇到一个常见的问题:从领域模型中获取数据。您可能希望在 UI 上呈现信息或向客户发送电子邮件,但如果您需要的信息隐藏在实体内部,则必须做出一些牺牲。您可能觉得有必要公开一个 getter,有时这可能是最佳选择。不过,有趣的是,备忘录模式确实为您提供了另一种选择。
If you buy into the guidance presented in this chapter that avoiding getters and setters helps to create behavior-rich domain models, you are actually quite likely to run into a common problem: getting data out of your domain models. You may want to present information on a UI or send e-mails to customers, but if the information you need is hidden away inside entities, something has to give. You may feel compelled to expose a getter, and sometimes that may be the best choice. Intriguingly, though, the memento pattern does give you another option.
使用备忘录模式,您可以创建实体状态的快照。该快照会被打包到实体的后门之外,以便您在应用程序的其他部分(例如 UI)中利用其数据。但您仍然保留了封装的一些好处,因为实体状态的结构仍然是私有的。本质上,备忘录代表了一个稳定的接口,应用程序的其他部分可以与之耦合,而实体的内部结构仍然可以几乎无阻碍地进行重构。
With the memento pattern, you create a snapshot of your entity’s state. The snapshot is bundled out the backdoor of your entity, allowing you to harness its data in other parts of your application, like the UI. But you still maintain some of the benefits of encapsulation, because the structure of the entity’s state is still private. In essence, mementos represent a stable interface that other parts of the application can become coupled to, while the innards of your entity can still be refactored with little friction.
电子商务领域模型中可能受益于纪念品模式的一部分是购物篮,如清单 16-20 所示。
One part of an e-commerce domain model that might benefit from the memento pattern is a shopping basket, as Listing 16-20 illustrates.
清单 16-20 中的Basket.GetSnapshot()提供了Basket实体状态的快照,即一个备忘录。通过这种方式暴露其数据,其内部状态(Id、Cost和Items)与应用程序的其余部分保持了隔离。
Basket.GetSnapshot() in Listing 16-20 provides a snapshot of the Basket entity’s state—that is, a memento. By exposing its data in this way, its internal state—Id, Cost, and Items—remains encapsulated from the rest of the application.
副作用会使代码更难推理和测试,而且它们常常是 bug 的根源。在广泛的编程环境中,尽可能避免产生副作用的函数通常被认为是一个好建议。你甚至在上一章中看到了无副作用和不可变性是值对象的两大主要优势。但是,如果说避免副作用是一个好建议,那么避免隐藏的副作用则是一项基本期望。
Side effects can make code harder to reason about and harder to test, and they can often be the source of bugs. In a broad programming context, avoiding side effecting functions as much as possible is generally considered good advice. You even saw in the previous chapter how being side-effect-free and immutable were two of the main strengths of value objects. But if avoiding side effects is good advice, avoiding hidden side effects is a fundamental expectation.
实体的许多操作很可能需要产生副作用。具体来说,实体上的许多方法可能需要更新其封装状态。但这种思维模式可能会导致不必要的习惯,使实体的所有方法都产生副作用,甚至更糟——成为隐藏副作用的温床。因此,你应该寻找机会,用无副作用的实现来替换那些有副作用的方法,例如清单 16-21 中所示的Dice.Value() 。
It is likely that many of an entity’s operations need to perform side effects. In particular, many methods on an entity likely need to update its encapsulated state. But this pattern of thinking can lead to the unnecessary habit of making all of an entity’s methods side effecting, and worse—home to hidden side effects. Accordingly, you should be on the lookout for opportunities to replace side effecting methods, like Dice.Value() shown in Listing 16-21, with side effect-free implementations instead.
当你专注于封装状态和创建行为丰富的领域模型时,很容易编写出像清单 16-21 中的Dice实体那样包含不必要副作用的代码。Dice的客户端可能会假设调用Value()将获取骰子上次掷出的值。这是因为Value()听起来像一个查询或属性。然而,正如你所见,调用Value()具有改变值的隐藏副作用。因此,Dice的客户端可能会调用Value()两次,期望看到相同的值,而没有意识到它的行为并非如此。
When you focus on encapsulating state and creating behavior-rich domain models, it can be easy to write code like the Dice entity in Listing 16-21 that houses unnecessary side effects. Clients of Dice are likely to assume that calling Value() will get the value of the dice the last time it was rolled. This is because Value() sounds like a query or property. As you can see though, calling Value() has the hidden side effect of changing the value. So a client of Dice may call Value() twice expecting to see the same value, without realizing it doesn’t behave that way.
示例 16-22 展示了一个更合理的实现。在这个例子中,Dice 有一个Roll()方法。Roll ( ) 听起来像一个命令,提醒客户端此方法有副作用。此外,还有一个 void 返回类型,使这一点更加清晰。你还可以看到Value现在是一个属性。它听起来像一个读取操作,并且它所做的就是读取骰子的当前值。没有隐藏的副作用。
A more sensible implementation is shown in Listing 16-22. In this example Dice has a Roll() method. Roll() sounds like a command, alerting clients that this method has side effects. In addition there is a void return type making this even clearer. You can also see that Value is now a property. It sounds like a read and faithfully all it does is read the current value of the dice. No hidden side effects.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 17 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 17 download and individually named according to the names throughout the chapter.
在构建领域模型时,有时会遇到一些概念或行为,它们在逻辑上与系统中的实体或聚合格格不入。这暗示你可能需要创建领域服务。
When building domain models, you sometimes come across concepts or behavior that do not logically sit comfortably within an entity or aggregate in the system. This is an implicit sign that you may need to create a domain service.
令人困惑的是,“服务”这个词的含义过于繁琐。不过,幸运的是,领域服务很容易根据两个定义特征进行区分:它们代表领域概念,并且无状态。您最有可能使用领域服务来编排实体并封装业务策略,而不是执行基础设施管道;将这些留给应用服务吧。
Confusingly, service is an overloaded term. Fortunately, though, domain services are easily distinguished based on two defining characteristics: they represent domain concepts, and they are stateless. You are most likely to use domain services to orchestrate entities and encapsulate business policies rather than carry out infrastructural plumbing; leave that to application services.
在本章中,你将学习如何构建适合不同场景的领域服务。这些场景包括将领域服务公开为在领域模型之外实现的契约,以及构建包含重要业务规则的纯领域服务。并完全驻留在领域模型中。本章还展示了一些从服务层和领域模型使用领域服务的方法。
In this chapter, you learn how to build domain services that cater to different scenarios. These scenarios include exposing domain services as contracts that are implemented outside the domain model, and building pure domain services that contain important business rules and reside entirely within the domain model. This chapter also shows some of the ways that your domain services can be consumed from both the service layer and the domain model.
在介绍具体示例之前,本章将详细阐述使用领域服务的动机,并进一步阐明它们到底是什么。
Before moving on to the concrete examples, this chapter elaborates on the motives for using domain services and provides further clarification of precisely what they are.
本书包含面向服务架构 (SOA)、应用服务和领域服务的信息。这些概念之间没有紧密联系,但开发人员和邮件列表中仍然存在许多关于它们之间区别的混淆。从概念上讲,领域服务代表领域概念;它们是存在于问题领域中的行为,它们出现在与领域专家的对话中,并且它们当然是通用语言 (UL) 的一部分。如果您能记住这些重要特征,您会发现领域服务是一个非常有用的工具。
This book contains information about Service Oriented Architecture (SOA), application services, and domain services. None of these concepts are closely related, yet there is still lots of confusion among developers and on mailing lists about the differences between them. Conceptually, domain services represent domain concepts; they are behaviors that exist in the problem domain, they occur in conversations with domain experts, and they are certainly part of the ubiquitous language (UL). If you can remember those important characteristics, you will find domain services to be a useful tool.
为了能够在代码中有效地建模领域服务,了解领域服务的一些技术特性非常重要。您很快就会了解这些特性。
A few technical characteristics of domain services are important to be aware of so that you can model them in your code effectively. You will learn about them shortly.
您曾与领域专家讨论过一个涉及多个实体的领域概念,但您不确定哪个实体“拥有”该行为。它似乎不属于任何一个实体,当您试图将其强行塞入任何一个实体时,都会显得很尴尬。这种思维模式强烈表明需要领域服务。
You’ve spoken to domain experts about a domain concept that involves multiple entities, but you’re unsure about which entity “owns” the behavior. It doesn’t appear to belong to any of them, and it seems awkward when you try to force-fit it onto either of them. This pattern of thinking is a strong indicator of the need for a domain service.
领域服务的主要关注点是执行一些涉及实体或值对象的行为。这可以用两个Competitor实体和一个OnlineDeathmatch实体来演示,它们都取自一个在线游戏系统的领域模型。它们分别如清单 17-1 和清单 17-2 所示。
A primary concern for domain services is carrying out some behavior involving entities or value objects. This can be demonstrated using two Competitor entities and an OnlineDeathmatch entity, all taken from the domain model of an online gaming system. They are shown in Listing 17-1 and Listing 17-2, respectively.
完成OnlineDeathMatch后,每个玩家的得分都会根据其在游戏中的表现进行更新。实现此逻辑的一个方法是将得分计算和奖励逻辑放在OnlineDeathmatch中,如清单 17-3 所示。
On completion of an OnlineDeathMatch, each player has his score updated based on how he performed in the game. One option for locating this logic is to place the score calculation and reward logic within the OnlineDeathmatch, as shown in Listing 17-3.
CalculateNewPlayerScores()作为OnlineDeathMatch的一部分,一个很大的问题是规则非常灵活;商家可能希望在特定时间奖励双倍积分或发放特殊奖励。在这种情况下,商家的意思是,分数和奖励计算本身就是至关重要的概念。相同的特殊促销活动通常适用于不同类型的游戏:TeamBattle、CaptureTheFlag、GangCrusade等等。
One big problem with CalculateNewPlayerScores() being part of the OnlineDeathMatch is that the rules are flexible; the business may want to award double points or hand out special prizes at certain times. What the business is saying in cases like this is that score and reward calculation are fundamentally important concepts within themselves. The same special promotions are often applied to different types of games: TeamBattle, CaptureTheFlag, GangCrusade, and so on.
这个场景暗示了需要领域服务来封装特定分数或奖励策略的单一职责。清单 17-4 展示了IGamingScorePolicy和IGamingRewardPolicy域服务接口,您可以将其注入并应用到OnlineDeathmatch或任何其他需要它们的域对象类型中。
This scenario is hinting at the need for domain services that encapsulate the single responsibility of a specific score or reward policy. Listing 17-4 shows the IGamingScorePolicy and IGamingRewardPolicy domain service interfaces that you can inject and apply into an OnlineDeathmatch or any other type of domain object that needs them.
使用清单 17-4 中的方法,您可以根据最新的业务促销活动切换特定的分数和忠诚度计算。同样重要的是,您现在可以在领域中明确这些概念。例如,当一家企业正在进行“高分会员免费 12 个月订阅”促销活动时,您可以在领域中将其明确建模为Free12MonthSubscriptionForHighScoresRewardPolicy领域服务,如清单 17-5 所示。
Using the approach in Listing 17-4, you can switch in and out specific score and loyalty calculations according to the latest business promotions. Equally as importantly, you can now make these concepts explicit in the domain. For example, when a business is running its “free 12-month subscription for high scores” promotion, you can model this explicitly in the domain as a Free12MonthSubscriptionForHighScoresRewardPolicy domain service, as shown in Listing 17-5.
清单 17-5 展示了Free12MonthSubscriptionForHighScoresRewardPolicy领域服务,它明确定义了 UL 中存在的一个重要领域概念。该领域服务包含重要的业务规则,位于领域模型内部,因此是纯粹的。然而,有些领域服务并非在领域模型中实现,而是在服务层中实现。
Listing 17-5 shows the Free12MonthSubscriptionForHighScoresRewardPolicy domain service that explicitly defines an important domain concept that exists within the UL. This domain service contains important business rules, lives inside the domain model, and hence is pure. However, some domain services are not implemented within the domain model; instead, their implementation lives in the service layer.
领域服务的另一个广泛用例是作为契约——概念本身对领域很重要,但实现依赖于领域模型中无法使用的基础设施。一个例子是ShippingRouteFinder,如清单 17-6 所示。您可以看到它向路由 API 发出 Web 请求,以便它可以找到给定旅程端点的可用路线。
The other broad use case for domain services is as a contract—where the concept itself is important to the domain, but the implementation relies on infrastructure that cannot be used in the domain model. An example of this is ShippingRouteFinder, shown in Listing 17-6. You can see that it makes a web request to the routing API so that it can find the available routes for the given journey endpoints.
重要的是,向 Web 服务器发出 HTTP 请求的逻辑不能存在于领域模型中,因为它涉及基础设施问题;因此,清单 17-6 中的ShippingRouteFinder领域服务的实现不能存在于领域模型中。但这个概念是 UL 的一部分,所以接口必须存在。
Importantly, the logic of making HTTP requests to a web server cannot live in the domain because it is an infrastructural concern; therefore, the implementation of the ShippingRouteFinder domain service in Listing 17-6 cannot live in the domain model. But the concept is part of the UL, so the interface must.
您可以将领域服务用作各种场景的契约,包括:
You can use domain services as contracts for a variety of scenarios, including:
领域服务有三个基本的技术特征:它们表示行为,因此没有身份;它们是无状态的;并且它们通常协调多个实体或领域对象。除了上一节中的示例之外,清单 17-7 中的RomanceOMeter也强调了这些特征。RomanceOMeter 的灵感来自于在线约会网站领域模型中的类似概念,它用于评估两个恋爱对象的兼容性。
Domain services have three fundamental technical characteristics: they represent behavior, and thus have no identity; they are stateless; and they often orchestrate multiple entities or domain objects. In addition to the examples in the previous section, the RomanceOMeter in Listing 17-7 highlights these characteristics. The RomanceOMeter is inspired by a similar concept in the domain model of an online dating website where it is used to assess how compatible two love seekers are.
清单 17-7 中的RomanceOMeter没有任何 ID 或与身份相关的状态。它是纯粹的行为,仅包含执行恋爱关系计算的AssessCompatibility()方法。正如你所见,它不保存任何状态,而是协调多个LoveSeeker实体;计算完全基于输入。这样一来,它满足了无状态的要求,并举例说明了领域服务如何协调其他领域对象。
There’s no ID or identification-related state on the RomanceOMeter in Listing 17-7. It is pure behavior, containing only AssessCompatibility() that performs the romance calculation. As you can also see, it keeps no state and orchestrates multiple LoveSeeker entities; the calculation is based entirely on the inputs. In doing so, it satisfies the stateless requirement and exemplifies how domain services orchestrate other domain objects.
在接受了并非所有领域逻辑都需要直接存在于实体上,并且领域服务是一个有用的概念之后,你需要注意不要将太多的逻辑推入领域服务中,这会导致不准确、混乱、贫乏的领域模型,并且概念内聚力低下。显然,这会抵消 DDD 的一些主要优势。不过,只要你足够注意,这不太可能成为问题。
After accepting that not all domain logic needs to live on entities directly and that domain services are a useful concept, you need to be careful not to push too much logic into domain services, which can lead to inaccurate, confusing, and anemic domain models with low conceptual cohesion. Clearly, that would counter some of DDD’s major benefits. However, as long as you pay enough attention, it’s unlikely to become a problem.
凭借经验,在领域服务过多和过少之间找到适当的平衡会更容易。但谨慎和逻辑思维能帮助您做出大多数正确的决策。例如,当向实体添加新行为不合适、感觉不对劲,并且与领域专家的说法不一致时,应该会想到使用领域服务。然后,您可以与这些领域专家进一步讨论这个问题,并听取他们如何描述这个概念。他们在讨论这个概念时是否总是引用实体,还是会孤立地讨论它?另一个极端是,如果您正在创建大量领域服务,那么您的思维过程可能有点过于自由。
Finding the right balance between too few and too many domain services is easier with experience. But care and logical thinking help you get the majority of decisions correct. For instance, one pattern of thinking that should evoke thoughts of using a domain service occurs when adding new behavior to an entity is an awkward fit and just doesn’t feel right, and it doesn’t align with what domain experts are saying. You can then discuss the issue further with those domain experts and listen to how they describe the concept. Do they always refer to an entity when discussing the concept, or do they discuss it in isolation? At the other extreme, if you are creating a lot of domain services, maybe your thought process is a little too liberal.
一个常见的混淆来源是区分应用程序服务和领域服务。然而,一旦你理解了两者的概念角色,你就不会再感到困惑了。正如你在本章中所看到的,领域服务表示存在于问题领域中的概念,并且至少它们的接口存在于领域模型中。相反,应用程序服务不表示领域概念,也不包含业务规则。此外,它们甚至不部分存在于领域模型中,甚至它们的接口也不存在于领域模型中。正如你将在第25章“命令:用于处理业务用例的应用服务模式”中看到的,应用程序服务位于服务层,并将事务等基础设施关注点整合在一起,以执行完整的业务用例。
A common source of confusion is differentiating application from domain services. Once you understand the conceptual role of both, though, you’ll never be confused again. As you’ve seen in this chapter, domain services represent concepts that exist within the problem domain, and at a minimum, their interface lives in the domain model. Conversely, application services do not represent domain concepts, and they don’t contain business rules. Also, they don’t even partially live in the domain model—not even their interfaces. As you will see in Chapter 25, “Commands: Application Service Patterns for Processing Business Use Cases,” application services live in the service layer and deal will pulling together infrastructural concerns, like transactions, to carry out full business use cases.
DDD 论坛上经常出现的一个问题是:在领域服务中,处理身份验证和授权的理想方式是什么?简单的答案是不需要,因为这是服务层的责任。这个例子典型地体现了围绕这两种服务类型的混淆,并通过了解为什么这是一个糟糕的问题(但由于“服务”这个术语的含义过于复杂,所以完全可以理解) ,帮助你理解每种服务的作用。
A question that appears regularly on DDD forums is: What’s the ideal way to handle authentication and authorization in a domain service? The simple answer is that you don’t, because that is a responsibility of the service layer. This example typifies the confusion around the two types of service and helps you understand the role of each by knowing why it is a bad question (but completely understandable due to the overloaded term service).
您可能仍然认为领域服务和应用服务相似,因为它们都可能需要处理基础设施问题。这里也很容易区分。领域服务依赖于用于通知领域逻辑的基础设施。相反,应用服务中的基础架构问题是为了确保领域模型能够正确执行。领域服务会向 Web 服务发出 HTTP 调用或将数据写入磁盘作为领域逻辑的一部分,而应用服务则会将领域模型包装在事务中或创建数据库连接,以便代码可以作为单个用例运行。
You may still be thinking that domain and application services are similar in that they both may have to deal with infrastructural concerns. It’s also easy to make a distinction here. Domain services rely on infrastructure that is used to inform domain logic. Conversely, infrastructural concerns in an application service are there to enable the domain model to execute correctly. A domain service makes HTTP calls to a web service or writes something to disk as part of domain logic, but an application service wraps the domain model in a transaction or creates database connections so that the code can run as a single use case.
了解了领域服务的概念并看过实际示例后,剩下的细节就是理解如何使用它们。有些选项几乎不需要解释,例如在应用服务中使用领域服务。然而,存在争议的是,领域服务通常需要作为完全驻留在领域模型中的领域流程中的一个步骤来使用。这引发了关于将领域服务注入实体的争论。正如您很快就会看到的,这是一个已经解决的问题,但任何解决方案都存在其利弊权衡和缺陷。
Having learned about the concept of domain services and seen practical examples, the remaining detail is in understanding how to use them. Some options require little explanation, as is the case with using domain services inside application services. Contentiously, though, domain services often need to be used as a step in domain processes that reside fully in the domain model. This leads to the debate of injecting domain services into entities. As you’ll see shortly, this is a solved problem, but no solution is without its trade-offs and detractors.
从最简单的情况开始,领域服务可以在应用服务中使用。作为完成完整业务用例的一部分,应用服务可以从存储库中提取相关实体并将它们传递到领域服务中,如清单 17-8 所示。
Starting with the easiest case, domain services can be put to use within application services. As part of its role in fulfilling a full business use case, an application service can pull the relevant entities out of a repository and pass them into a domain service, as shown in Listing 17-8.
在清单 17-8 中,MultiMemberInsurancePremium应用服务将IMultiMemberPremiumCalculator与其CalculatePremium()所需的Policy和Member实体组合在一起。这说明,在服务层使用时,领域服务和其他领域对象(如实体)可以根据需要轻松地组合在一起。然而,这种便利性并非总是可实现的,例如在实体似乎依赖于领域服务并且领域对象需要在领域模型内进行协调的情况下。
In Listing 17-8, the MultiMemberInsurancePremium application service pieces together the IMultiMemberPremiumCalculator with the Policy and Member entities that are required by its CalculatePremium(). This illustrates that, when used in the service layer, domain services and other domain objects like entities can be easily pieced together as needed. This convenience may not always be achievable, though, like in cases where an entity appears to be dependent on a domain service and the domain objects need to be coordinated within the domain model.
有时,实体需要领域服务以一种避免在应用服务中将它们拼凑在一起的方式执行其行为。一个典型的例子是,当实体执行某些任务后需要发出通知。清单 17-9 突出显示了这种类型的场景,展示了一个RestaurantBooking 实体,当顾客确认餐厅预订时,它会触发 NotifyBookingConfirmation 。RestaurantBooking 实体直接依赖于 IRestaurantNotifier领域服务来通知餐厅预订确认。
Sometimes an entity needs a domain service to carry out its behavior in a way that precludes piecing them together in an application service. A typical example is when a notification needs to occur following an entity executing some task. Listing 17-9 highlights this type of scenario by showing a RestaurantBooking entity that triggers a NotifyBookingConfirmation when a customer confirms a restaurant booking. The RestaurantBooking entity directly depends on the IRestaurantNotifier domain service to notify the restaurant of the booking confirmation.
在清单 17-9 中,挑战在于让restaurantNotifier ( IRestaurantNotifier的一个实例)在ConfirmBooking()的范围内可用。使用普通的旧构造函数注入作为显而易见的选择似乎很容易。然而,事情并不总是那么简单;通常使用对象关系映射器 (ORM) 来管理实体的生命周期,从而消除了开发人员在对象构造时传递依赖项的能力。以下每种技术都旨在缓解这个问题。
In Listing 17-9, the challenge is to have the restaurantNotifier, an instance of IRestaurantNotifier, available within the scope of ConfirmBooking(). It may seem easy with plain old constructor injection as the obvious choice. It’s not always that straightforward, though; often object-relational mappers (ORM)s are used to manage the life cycle of entities, removing the developer’s ability to pass in dependencies at object construction. Each of the following techniques aims to alleviate this problem.
如果实体或其他领域对象依赖于领域服务,则可以将相关服务传递给构造函数(如果您自己管理对象构造)。清单 17-10 举例说明了使用工厂方法解决此问题的理想解决方案。
If an entity or other domain object depends on a domain service, you can pass the relevant service into the constructor if you are managing object construction yourself. Listing 17-10 exemplifies the desirable solution of using a factory method to solve this problem.
清单 17-10 展示了如何将RestaurantNotifier领域服务传递到RestaurantBooking实体的构造函数中,该实体是在CreateBooking()工厂方法中手动构建的。工厂是许多代码库中常见的模式。到目前为止一切顺利。但是,当您不自己处理实体的构造时(通常是当 ORM 从持久层加载实体时),这种解决方案在 DDD 中就会出现问题。
Listing 17-10 shows how you can pass a RestaurantNotifier domain service into the constructor of the RestaurantBooking entity that is manually constructed inside the CreateBooking() factory method. Factories are a common pattern that you see in many codebases. So far so good. But this solution becomes problematic in DDD when you don’t handle construction of the entity yourself—typically when an ORM loads an entity from persistence.
您仍然可以绕过 ORM 管理的对象生命周期,但解决方案不一定完美。清单 17-11 展示了工厂方法如何添加对象构造的第二阶段,即在 ORM 构造实体后,将领域服务设置为实体的属性。类似地,您可以使用Init()方法获取所有依赖项并进行设置,就像伪构造函数一样。无论哪种方式,您都可能忘记对象的构造分为两个阶段,这可能导致生产系统的状态不一致,并带来许多令人头疼的问题。
You can still work around an object’s life cycle that an ORM is managing, but the solution is not necessarily pretty. Listing 17-11 shows how your factory methods can add a second stage of object construction that sets the domain service as a property on the entity after an ORM has constructed it. In a similar fashion, you can have an Init() method that takes all the dependencies and sets them, like a pseudo-constructor. Either way, you’re open to the problem of forgetting that construction of the object occurs in two phases, which can lead to inconsistent states in production systems with lots of ensuing head scratching.
您可能已经想到了与两阶段构造相关的其他问题,例如记住在整个代码库中一致地应用模式,并严格确保在构造对象后不再调用Init() 。
You may have thought of other problems associated with two-phase construction, such as remembering to apply the pattern consistently across the codebase and being disciplined to ensure that Init() is not called again after the object has been constructed.
如果您不想进行所有这些手动配置,您可能更愿意使用依赖注入将其中一些配置卸载到控制反转 (IoC) 容器中。
If all this manual configuration is undesirable for you, you may prefer to offload some of it to an Inversion of Control (IoC) container by using dependency injection.
另一种方法是将领域服务依赖注入到需要使用它们的实体中。将领域服务添加为构造函数参数后,剩下的任务就是使用标准依赖注入连接所需的实现。使用依赖注入可以省去手动构造对象的麻烦。不过,选择这种方式还是手动连接依赖关系主要是一个风格问题,因为处理 ORM 的问题可能仍然存在。
Another approach is to dependency-inject domain services into the entities that need to use them. Once a domain service has been added as a constructor parameter, the remaining task is to wire up the desired implementation using standard dependency injection. Using dependency injection saves you the hassle of manually constructing objects. It’s mainly a question of style whether you choose this or manually wiring up your dependencies, though, because the problem of dealing with ORMs may still be present.
某些 ORM 可以连接 IoC 容器管理的依赖项。然而,遗憾的是,很多 ORM 无法做到这一点。不过,如果您仍然坚持使用 IoC 容器和 ORM,服务定位器模式是一个潜在的解决方案。
With some ORMs, it’s possible to have them wire up dependencies that your IoC container manages. Unfortunately, though, with many it is not. However, if you’re still determined to use an IoC container and an ORM, the service locator pattern is one potential solution.
提前警告:服务定位器模式颇具争议;您很快就会了解原因。但是,您可以在清单 17-12 中看到它如何解决允许 IoC 容器处理由 ORM 加载的实体的依赖关系设置的问题,而无需手动干预。
Advance warning: the service locator pattern is fairly controversial; you’ll learn why shortly. However, you can see in Listing 17-12 how it solves the problem of allowing IoC containers to handle setting the dependencies of entities that are loaded by ORMs, without the need for manual intervention.
通过对清单 17-12 中所示的ServiceLocator类进行硬编码依赖,可以在 ORM 初始化实体后,在对象构造期间从 IoC 容器加载实体的所有依赖项。这消除了手动构造步骤的需要。但这是有代价的;现在实体与ServiceLocator紧密耦合——这是一个基础设施问题,理想情况下,您不希望它污染领域模型。
By taking the hard-coded dependency on the ServiceLocator class shown in Listing 17-12, you can load all of an entity’s dependencies from an IoC container during object construction after an ORM has initialized it. This removes the need for manual construction steps. But there’s a price; now the entity is tightly coupled to the ServiceLocator—an infrastructural concern that ideally you don’t want polluting the domain model.
使用服务定位器的真正问题在于紧密耦合;例如,你必须在测试中使用 mock 函数。另一个问题是对象的依赖关系被模糊化,因为它们不再被传入构造函数。
The real problems with using a service locator arise from the tight coupling; you have to mock mocks in your tests, for example. Another problem is the obscuring of an object’s dependencies, because they’re no longer passed into the constructor.
如果您愿意放弃在构造时将领域服务传递给实体,则可以选择使用双重分派模式将它们作为方法参数传递。使用双重分派,领域服务被传递到实体的方法中,然后实体将自身传递给领域服务的方法中,如清单 17-13 所示。
If you’re happy to forego passing domain services into entities at construction time, you have the option of instead passing them as method arguments using the double dispatch pattern. With double dispatch, a domain service is passed into a method on an entity, and the entity then passes itself into a method on the domain service, as shown in Listing 17-13.
如清单 17-13 所示,领域服务不再需要提供给实体的构造函数。相反,它们可以由应用服务传递到实体的方法中。由于一些原因,这种方法并没有被证明非常流行。有些人认为将依赖项传递给方法需要方法的调用者提供依赖项——这不应该属于他们。另一些人则认为依赖项通常通过构造函数传递,因为它们不应该成为方法签名的一部分;它们是可以更改的实现细节,而方法签名不应该如此不稳定。
As Listing 17-13 shows, domain services no longer need to be supplied into an entity’s constructor. Instead, they can be passed into methods on an entity by an application service. This approach hasn’t proven to be massively popular for a few reasons. Some feel that passing dependencies into methods requires callers of the method to supply the dependency—a responsibility that shouldn’t belong to them. Others argue that dependencies normally go via the constructor because they should not be part of a method’s signature; they’re an implementation detail that may be changed, whereas the method signature should not be so volatile.
因此,双重调度并非人人都喜欢,但目前为止提出的其他方案也并非人人都喜欢。最好还是亲自探索一下这个想法,并评估它在你的项目中可能如何运作。然而,一种流行的方法是使用领域事件模式,承诺真正将实体与领域服务分离。
So double dispatch is not to everyone’s liking, but neither are any of the other options presented so far. It’s definitely best to explore the idea yourself and assess how it may work on your projects. One approach that has become popular, though, is using the domain events pattern, with the promise of truly decoupling entities from domain services.
一个有趣的模式是领域事件,它完全避免了将领域服务注入实体的需要。当重要操作发生时,实体可以引发领域事件,并由注册该事件的订阅者处理。正如你可能已经猜到的那样,领域服务可以驻留在订阅者中,因此,它不存在于实体中。
An interesting pattern that completely avoids the need for injecting domain services into entities is domain events. When important actions occur, an entity can raise a domain event that is handled by subscribers who are registered for that event. As you might have guessed, domain services can reside within a subscriber, and therefore, not an entity.
清单 17-14 突出显示了RestaurantBooking实体引发BookingConfirmedByCustomer领域事件的情况。清单 17-15 显示了一个事件处理程序(也称为订阅者),它通过调用IRestaurantNotifier领域服务来处理此类事件。最后,清单 17-16 展示了将这些组件拼接在一起所需的少量工作。
Listing 17-14 highlights the RestaurantBooking entity raising a BookingConfirmedByCustomer domain event. Listing 17-15 shows an event handler, also known as a subscriber, which handles this type of event by invoking the IRestaurantNotifier domain service. Finally, Listing 17-16 shows the small amount of work required to stitch these components together.
清单 17-14 至 17-16 展示了将领域事件模式应用于正在进行的餐厅预订示例的最小示例。第 18 章“领域事件”专门介绍领域事件模式,并提供了更深入的示例来探讨该模式的优缺点。您可能已经从清单 17-14 至 17-16 中看出一个明显的缺点:逻辑现在分布在RestaurantBooking实体和NotifyRestaurantOnCustomerBookingConfirmation事件处理程序之间,而在其他模式中,这将是一段连续的代码。
Listings 17-14 to 17-16 show a minimum possible example of the domain events pattern applied to the ongoing restaurant booking example. Chapter 18, “Domain Events,” is dedicated to the domain events pattern and provides more in-depth examples that explore the strengths and weaknesses of the pattern. One obvious drawback that you may have discerned from Listings 17-14 to 17-16 is that the logic is now distributed between the RestaurantBooking entity and the NotifyRestaurantOnCustomerBookingConfirmation event handler, whereas with other patterns this would be a single piece of sequential code.
在上例中,您看到了领域事件如何阻止将领域服务注入实体,而其他模式则朝着相反的方向发展,试图找到一种使依赖关系可行的方法。后者在 DDD 社区中是一种颇具争议的方法;许多从业者认为这是一个糟糕的主意。最终,您需要结合具体情况、个人偏好和经验来决定哪种方案最适合您。
In the previous example, you saw how domain events preclude injecting domain services into entities, whereas each of the other patterns goes in the opposite direction and tries to find a way to make the dependency feasible. The latter is a fairly contentious approach within the DDD community; many practitioners argue that it’s just a bad idea. Ultimately, you need to use context, personal preferences, and your experience to decide which option you like the best.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 18 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 18 download and individually named according to the names throughout the chapter.
领域驱动设计 (DDD) 从业者发现,通过了解问题域中发生的事件(而不仅仅是实体),他们可以更好地理解问题域。这些事件称为领域事件,将在与领域专家的知识梳理会议期间发现。发现领域事件非常有价值,因此 DDD 从业者创新了知识梳理技术,使用事件风暴 ( http://ziobrando.blogspot.co.uk/2013/11/introducing-event-storming.html#.U5YPynU-mHs )等实践使其更加以事件为中心。然而,这些创新也带来了新的挑战。既然概念模型以事件为中心,代码也需要以事件为中心,这样它才能表达概念模型。这就是领域事件设计模式的价值所在。
Domain-Driven Design (DDD) practitioners have found that they can better understand the problem domain by learning about the events that occur within it—not just the entities. These events, known as domain events, will be uncovered during knowledge-crunching sessions with domain experts. Uncovering domain events is so valuable that DDD practitioners have innovated knowledge-crunching techniques to make them more event-focused using practices such as event storming (http://ziobrando.blogspot.co.uk/2013/11/introducing-event-storming.html#.U5YPynU-mHs). With these innovations, though, come new challenges. Now that conceptual models are event-centric, the code also needs to be event-centric so it can express the conceptual model. This is where the domain events design pattern adds value.
第二部分重点介绍了使用消息总线或 REST 等传输方式在有界上下文之间以异步消息形式发送的事件。然而,领域事件设计模式通常在有界上下文的领域模型中用作单线程模式。在本章中,您将看到一些示例,探讨使用领域事件时所涉及的不同实现选项和总体权衡。
Part II focused on events that are sent as asynchronous messages between bounded contexts using transports like a message bus or REST. However, the domain events design pattern is generally used as a single-threaded pattern inside a domain model within a bounded context. In this chapter you will see examples that explore the different implementation options and overall trade-offs involved when using domain events.
在开始之前,务必认识到使用领域事件并不一定需要使用事件溯源。不幸的是,这是一个常见的误解;使用传统的持久化方案(例如 SQL 数据库)完全可以。然而,将事件溯源或异步消息传递与使用领域事件的领域模型结合使用通常是一个不错的选择。第 22 章“事件溯源”将阐明这种关系。
Before getting started, it’s important to acknowledge that using domain events does not necessitate using event sourcing. Unfortunately, this is a common misconception; it is entirely possible to use traditional persistence options like a SQL database. However, applying event sourcing or asynchronous messaging in combination with a domain model that uses domain events is often a good combination. Chapter 22, “Event Sourcing,” will clarify this relationship.
本章将使用领域事件的每种实现模式来实现在线披萨外送服务中的一个用例。这样,您可以看到直接的比较。建模的用例是配送保证,如果顾客的披萨未在特定时间内送达,他们将获得折扣。
Throughout this chapter, each implementation pattern for domain events will be used to implement a single use case from an online pizza delivery service. This way you can see direct comparisons. The use case being modeled is the delivery guarantee, where a customer will receive a discount if their pizza is not delivered in a certain timeframe.
如果您了解发布-订阅模式或 C# 事件,就能很快掌握领域事件模式。本质上,您使用基础架构组件(有时来自领域模型内部)来发布事件。默认情况下,事件会由已注册该类型事件的每个事件处理程序在同一线程内同步处理。此模式也可以实现异步事件处理。
If you understand the publish-subscribe pattern or C# events, you will grasp the domain events pattern very quickly. Essentially, you use an infrastructural component, sometimes from within your domain model, to publish events. By default, events are then processed synchronously inside the same thread by each event handler that has been registered for that type of event. Asynchronous event handling is also possible with this pattern.
事件只是具有公共属性(数据对象、普通老式 C# 对象 [POCO]、普通老式 Java 对象 [POJO])的不可变类,它们代表问题域中的重要事件。清单 18-1 展示了一个DeliveryGuaranteeFailed事件,当披萨外送时间超过承诺时间时,该事件会在域中触发。
Events are just immutable classes with public properties (data objects, Plain Old C# Objects [POCOs], Plain Old Java Objects [POJOs]) representing important events in the problem domain. Listing 18-1 shows a DeliveryGuaranteeFailed event, which is fired in the domain when a pizza delivery took longer than promised.
请注意,在清单 18-1 中,DeliveryGuaranteeFailed事件只是一个具有属性的类;这就是您的事件所需要的全部内容。
Notice in Listing 18-1 that the DeliveryGuaranteeFailed event is just a class with properties; this is all that your events need to be.
为了响应事件,事件处理程序会被执行。清单 18-2 展示了如何将事件处理程序注册为在引发事件(DeliveryGuaranteeFailed)时调用的方法( onDeliveryFailure )。
In response to events, event handlers are executed. Listing 18-2 shows how you can register an event handler as a method to be called (onDeliveryFailure) when the event (DeliveryGuaranteeFailed) is raised.
使用领域事件模式可以与异步工作流(包括有界上下文之间的通信)协同工作。正如您在第二部分关于有界上下文集成中所看到的,有界上下文之间的异步消息是一种有助于提高可靠性和可扩展性的现代解决方案。您可以从领域事件的事件处理程序中触发这些流程。有时,您甚至可能希望在有界上下文中进行异步、可靠的通信,例如实现最终一致的聚合。利用领域事件的解耦特性,可以在这两种情况下为您提供帮助。
Using the domain events pattern can be synergistic with asynchronous workflows, including communication between bounded contexts. As you saw in Part II on bounded context integration, asynchronous messages between bounded contexts are a modern solution that help with reliability and scalability. You can trigger these processes from within your domain events event handlers. Sometimes you may even want asynchronous, reliable communication within a bounded context, for scenarios like implementing eventually consistent aggregates. Using the decoupled nature of domain events can help you in both scenarios.
重要的是,在创建触发异步工作流的事件处理程序时,需要明确事务边界。例如,如果一个事件处理程序更新数据库,而另一个事件处理程序发布消息,则您希望在其中一个操作失败时这两个操作都回滚,如图18.1所示。
Importantly, when creating event handlers that trigger asynchronous workflows, you need to be clear about transactional boundaries. For example, if one event handler updates the database, and another publishes a message, you would want both operations to roll back if either of them failed, as Figure 18.1 shows.
图 18.1确保正确的事务行为
FIGURE 18.1 Ensuring correct transactional behavior
使用领域事件模式时,需要注意一个重要的区别,以避免产生可能导致技术实现不佳的混淆。务必了解内部事件和外部事件之间的区别。内部事件是领域模型内部的事件——它们不会在有界上下文之间共享。在本章中,您将了解领域事件模式如何使用内部事件,而您在本书第二部分中了解的是外部事件。
An important distinction needs to be made when using the domain events pattern to avoid confusion that can lead to poor technical implementations. It is crucial that you are aware of the difference between internal and external events. Internal events are internal to a domain model–they are not shared between bounded contexts. In this chapter, you will see how the domain events pattern uses internal events, whereas you saw external events in Part II of this book.
区分内部事件和外部事件非常重要,因为它们具有不同的特性。由于内部事件的作用域仅限于单个有界上下文,因此可以将领域对象放置在其上,如清单 18-1 中的示例所示。这不会带来任何风险,因为其他有界上下文无法与这些领域对象耦合。相反,外部事件的结构往往比较扁平,只暴露少量属性——大多数情况下只是一些相关的 ID,如清单 18-3 所示。
Differentiating internal and external events is important because they have different characteristics. Because internal events are limited in scope to a single bounded context, it is ok to put domain objects on them, as the example in Listing 18-1 showed. This poses no risk, because other bounded contexts cannot become coupled to these domain objects. Conversely, external events tend to be flat in structure, exposing just a few properties—most of the time just correlational IDs, as typified in Listing 18-3.
您在第二部分中了解到,外部事件需要进行版本控制,以避免重大更改。这是它与内部事件的另一个区别,因为如果您对内部事件进行重大更改,您的代码将无法编译(如果使用编译型编程语言)。因此,无需对内部事件进行版本控制。
You learned in Part II that external events need to be versioned to avoid breaking changes. This is another differentiator with internal events, because if you make breaking changes to an internal event your code will not compile (if using a compiled programming language). So there’s no need to version internal events.
当你开始实现领域事件时,你会发现在一个典型的业务用例中,可能会引发多个内部事件,以及由服务层引发的一两个外部事件。图 18.2展示了典型用例中事件发生的顺序。
As you start to implement domain events, you will see that in a typical business use case there may be a number of internal events raised, and just one or two external events that are raised by the service layer. Figure 18.2 illustrates how the sequence of events may occur in a typical use case.
图 18.2典型业务用例中的内部和外部事件流
FIGURE 18.2 Flow of internal and external events in a typical business use case
考虑到所有这些差异,将事件放在不同的命名空间中以区分内部事件和外部事件是有意义的。
With all of these differences in mind, it makes sense to put your events in different namespaces to accentuate those that are internal from those that are external.
在查看领域事件模式的具体示例之前,务必先了解领域层事件处理程序和应用服务层事件处理程序之间的区别。尽管它们看起来相同,但它们的职责却截然不同。领域事件处理程序调用领域逻辑,例如调用领域服务;而应用服务层事件处理程序本质上更侧重于基础架构,执行诸如发送电子邮件和将事件发布到其他有界上下文之类的任务。
Before seeing concrete examples of the domain events pattern it’s important to have an understanding of the distinction between handlers in the domain and handlers in the application service layer. Even though they look the same, their responsibilities are significantly different. Domain event handlers invoke domain logic, such as invoking a domain service, whereas application service layer event handlers are more infrastructural in nature, carrying out tasks like sending e-mails and publishing events to other bounded contexts.
领域模型中存在的事件处理程序可以处理该模型中发生的事件。这些场景是对问题域中发生的交互序列进行建模。例如,在线外卖商店中,订单验证器会验证订单,确认客户不在黑名单中在之前的订单付款失败后。验证器会触发自身的事件,以触发业务流程的下一部分。领域事件处理程序通常会委托给领域服务。
Event handlers that exist within the domain model can handle events that occur there. These scenarios are modeling sequences of interaction that occur in the problem domain. For example, in the online takeaway store, an order validator validates an order, verifying that the customer is not blacklisted after failing to pay for previous orders. The validator then raises events of its own to trigger the next part of the business process. It’s common to see domain event handlers delegating to a domain service.
除了位于域中的事件处理程序之外,在应用服务层中也拥有事件处理程序也有好处。这些事件处理程序通常执行诸如发送电子邮件之类的基础任务。请注意,这些处理程序不属于 UL 或域。
There is a benefit to having event handlers that live in the application service layer in addition to those that live in the domain. These event handlers tend to carry out infrastructural tasks like sending e-mails. Note that these handlers are not part of UL or the domain.
应用服务层处理程序的一项重要职责是,使用第二部分介绍的技术触发与外部有界上下文的通信。它们还管理与外部服务(例如支付网关)的通信。
One important responsibility of application service layer handlers is that they trigger communication with external bounded contexts, using the techniques presented in Part II. They also manage communication with external services, like payment gateways.
领域事件是一种通用模式,它基本上是在发布-订阅模式的基础上添加了领域语义。这赋予了您很大的自由度来实现解决方案。您将在以下示例中看到各种选项,从同步使用原生语言结构到基于消息总线的替代方案。
Domain events is a generic pattern that basically adds domain semantics to the publish-subscribe pattern. This gives you a lot of freedom to implement the solution. You’ll see a variety of options in the following examples that range from synchronous use of native language constructs, to message bus-based alternatives.
开始使用领域事件的一个简单方法是依赖你正在使用的语言或平台提供的原生功能(如果适用)。在 C# 中,这意味着使用event关键字。
An easy way to get started with domain events is to rely on the native capabilities provided by the language or platform you are using, if applicable. In C# this means using the event keyword.
要使用event关键字定义域事件,可以在以域事件命名的实体上创建一个公共字段。还需要创建一个代表事件契约的委托。清单 18-4 显示了这两个细节。
To define a domain event using the event keyword, you can create a public field on an entity named after the domain event. You also need to create a delegate that represents the contract of the event. Both of these details are shown in Listing 18-4.
在清单 18-4 中,使用event关键字创建了一个名为DeliveryGuaranteeFailed的事件。可以看到,该事件的类型为DeliveryGuaranteeFailedHandler,这意味着DeliveryGuaranteeFailed事件的所有事件处理程序都必须具有与DeliveryGuaranteeFailedHandler相同的签名。您可以在上面使用delegate关键字声明的行中看到DeliveryGuaranteeFailedHandler的签名。它的签名是一个 void 返回类型,带有一个DeliveryGuaranteeFailed类型的参数。您可以在清单 18-5 中看到一个具有此签名的处理程序正在注册。
In Listing 18-4 an event called DeliveryGuaranteeFailed is created using the event keyword. You can see that this event has a type of DeliveryGuaranteeFailedHandler signifying that all event handlers of the DeliveryGauranteeFailed event must have the same signature as the DeliveryGuaranteeFailedHandler. You can see the signature of the DeliveryGuaranteeFailedHandler on the line above where it is declared using the delegate keyword. Its signature is a void return type with a single parameter of type DeliveryGuaranteeFailed. You can see a handler with this signature being registered in Listing 18-5.
onDeliveryGuaranteeFailed是一个返回类型为 void 的方法,它接受一个DeliveryGuaranteeFailed类型的参数。因此,它可以成功注册为DeliveryGuaranteeFailed事件的处理程序,如清单 18-5 所示。它使用 C# 注册事件处理程序的特殊语法:+=。
onDeliveryGuaranteeFailed is a method with a void return type that takes a single parameter of type DeliveryGuaranteeFailed. Therefore it can successfully be registered as a handler for the DeliveryGuaranteeFailed event, as shown in Listing 18-5. It is registered using C#’s special syntax for registering event handlers: +=.
要引发 C# 事件,您可以像调用方法一样调用它们,如清单 18-6 所示,其中引发了DeliveryGuaranteeFailed事件。首先检查事件是否为空非常重要,因为如果事件没有订阅者,它将为空,从而导致抛出空引用异常。
To raise C# events you invoke them like methods, as shown in Listing 18-6, where the DeliveryGuaranteeFailed event is raised. It’s important to check if the event is null first, because if an event has no subscribers it will be null, causing a null reference exception to be thrown.
要触发异步进程,您可以创建并注册一个事件处理程序,用于发布异步消息、写入队列或更新数据库。您需要确保该操作已列入当前事务的一部分,这样,如果事务回滚,触发异步进程的操作也会回滚。使用 C# 和event关键字时,您可能需要配置分布式事务协调器 (DTC)。
To trigger an asynchronous process you can create, and register, an event handler that publishes an asynchronous message, writes to a queue, or updates a database. You need to ensure that the action is enlisted as part of the current transaction, so that if the transaction rolls back, so will the action that triggers the asynchronous process. When using C# and the event keyword, it’s likely you will want to configure the Distributed Transaction Coordinator (DTC).
从清单 18-4 到清单 18-6 可以看出,仅使用event关键字就可以最大限度地减少使用原生事件的前期投入。然而,这种方法是有代价的;事件的发布者和订阅者之间存在紧密的耦合。清单 18-5 中可以看到这一点,其中事件的处理程序必须知道哪个对象正在发布事件。领域事件模式的替代实现具有更松散的发布者和订阅者耦合,如下一节所示。
As you can see from Listings 18-4 to 18-6, the up-front investment when using native events can be minimal by just using the event keyword. However, this approach has a cost; there is a tight coupling between publishers of an event and subscribers. You can see this in Listing 18-5, where the handler of an event must know which object is publishing the event. Alternative implementations of the domain events pattern have a looser coupling of publishers and subscribers, as shown in the next section.
为了解耦发布者和订阅者,您可以使用内存消息总线来实现领域事件模式。假设消息总线同时支持同步和异步传输,那么当系统某些方面能够从中受益时,您可以轻松地将其异步化。NServiceBus 就是一款同时提供同步和异步传输选项的消息总线。
To decouple publishers and subscribers you can implement the domain events pattern using an in-memory message bus. This gives you the option to easily make some aspects of the system asynchronous when they can benefit from it, assuming the message bus supports both synchronous and asynchronous transit. NServiceBus is one message bus that does provide both options.
使用消息总线,您再次关注事件和处理程序。但不是将它们相互连接,而是通过总线发布事件,并通过总线注册处理程序。然而,使用 NServiceBus 则无需实际注册订阅者,因为所有事件处理程序都实现了 NServiceBus 的IHandleMessages<T>接口,其中T是正在处理的事件的类型,因此 NServiceBus 可以轻松地自动找到事件处理程序。NServiceBus 所需要的只是被告知哪些类是事件,如清单 18-7 所示。
With a message bus you again focus on events and handlers. But instead of wiring them up with each other, you publish events through the bus, and register handlers through the bus. With NServiceBus there is no need to actually register subscribers, though, because all event handlers implement the NServiceBus IHandleMessages<T> interface, where T is the type of event being handled, so NServiceBus can easily find event handlers automatically. All NServiceBus requires is to be told which classes are the events, as shown in Listing 18-7.
清单 18-7 遵循了这样的约定:任何位于名为Events的命名空间内的类都将被 NServiceBus 视为事件。因此,如果清单 18-8 中的事件已发布(注意其命名空间包含Events),NServiceBus 将调用清单 18-9 所示的处理程序。
Listing 18-7 uses the convention that any class inside a namespace called Events will be considered an event by NServiceBus. Therefore, if the event in Listing 18-8 was published (notice its namespace contains Events), NServiceBus would invoke the handler shown in Listing 18-9.
清单 18-10 展示了如何使用 NServiceBus 的内存总线发布此消息。请注意,订阅者并不知道或不关心事件的发布者是谁,反之亦然。
Listing 18-10 then shows how this message is published using NServiceBus’s in-memory bus. Notice how the subscriber does not know, or care, who the publisher of the event is and vice-versa.
使用内存总线的一个棘手细节是如何在领域模型中传递它。理想情况下,你不希望像消息总线这样的技术问题让领域模型变得杂乱无章。另一方面,你确实需要某种方式来发布事件。对于某些团队来说,在这种情况下,允许技术问题进入领域模型是一个可以接受的权衡,因为它可以让模型更全面地表达领域概念。
One of the tricky details with using an in-memory bus is passing it around the domain model. Ideally you don’t want technical concerns, like a message bus, cluttering the domain model at all. On the other hand, you do need some way of publishing events. For some teams, this is a situation where it is an acceptable trade-off to allow technical concerns into the domain because it allows the model to be more expressive of domain concepts overall.
NServiceBus 的默认操作模式是发送异步消息,因此如果您决定需要异步触发某些领域事件,转换起来相对容易。您可以添加一个新的事件处理程序,订阅内存中的事件并发布异步事件。或者,您可以使用Bus.Publish()而不是Bus.InMemory.Raise()异步发布初始事件,在这种情况下,所有订阅者都会在不同的线程上异步执行。
NServiceBus’s default mode of operation is sending asynchronous messages, so if you decide that you need to fire some domain events asynchronously, the transition is relatively easy. You add a new event handler that subscribes to an in-memory event and publishes asynchronous events. Or, you could just publish the initial event asynchronously using Bus.Publish() instead of Bus.InMemory.Raise(), in which case all subscribers would execute asynchronously on a different thread.
在决定异步发布事件时,务必注意事务要求。如果您有两个事件处理程序需要原子地成功或失败,则需要确保它们同步执行。否则,您将容易受到不一致引起的错误的影响。
It’s important to be aware of transactional requirements when deciding to publish events asynchronously. If you have two event handlers that need to atomically succeed or fail, then you will need to ensure they execute synchronously. Otherwise you will be open to bugs arising from inconsistency.
传统的 DDD 建议是从领域模型中移除所有基础设施关注点。传统的面向对象指南警告应避免依赖静态方法,因为静态方法会导致耦合。你可能会惊讶地发现,领域事件模式的另一个流行版本依赖于一个名为DomainEvents的静态类,该类处理基础设施关注点,并且位于领域模型中。清单 18-11 展示了静态DomainEvents类的实现,该类基于 DDD 专家 Udi Dahan 的工作(http://www.udidahan.com/2008/08/25/domain-events-take-2/)。
Traditional DDD advice has been to remove all infrastructural concerns from the domain model. Traditional object-oriented guidelines warn that you should avoid relying on static methods because of coupling. You might be surprised to hear then that another popular version of the domain events pattern relies on a static class called DomainEvents that deals with infrastructural concerns and lives in the domain model. Listing 18-11 shows the implementation of the static DomainEvents class, which builds on the work of DDD expert Udi Dahan (http://www.udidahan.com/2008/08/25/domain-events-take-2/).
DomainEvents有两种重要方法:Register<T>将注册一个回调,当另一个重要方法Raise<T>引发类型 T 的事件时执行该回调。
DomainEvents has two important methods: Register<T> will register a callback that is executed when an event of type T is raised by the other important method: Raise<T>.
清单 18-11 中需要注意的另一个细节是私有DomainEventsRegistrationRemover类的使用。您可以看到,Raise<T>使用此类的实例在事件处理程序执行一次后自动注销它们。
Another detail to discern in Listing 18-11 is the use of the private DomainEventsRegistrationRemover class. You can see that Raise<T> uses an instance of this class to automatically unregister event handlers once they have executed a single time.
清单 18-12 展示了如何使用DomainEvents注册事件处理程序,而清单 18-13 展示了如何发布事件。
Listing 18-12 shows how you can register an event handler with DomainEvents while Listing 18-13 then shows how you can publish an event.
使用静态DomainEvents类的一大挑战是管理线程问题。这是因为任何在一个线程中注册的处理程序都会在以下类型的事件发生时执行:它处理的事件是由任何其他线程发布的。正如前面提到的,这是因为DomainEvents是静态的;每个线程都使用同一个实例。
One of the big challenges involved with using the static DomainEvents class is managing threading issues. This is because any handler registered in one thread is executed when an event of the type it handles is published by any other thread. As alluded to, this is a result of DomainEvents being static; every thread uses the same instance.
一种有助于避免线程问题的技术是将ThreadStatic属性应用于DomainEvents类的处理程序集合和回调集合。使用此属性,每个线程都会获得自己的集合,这意味着在一个线程上注册的处理程序对另一个线程不可见。清单 18-14 展示了如何将ThreadStatic属性与领域事件一起使用。
A technique that can help to avoid threading issues is to apply the ThreadStatic attribute to the collection of handlers and collection of callbacks on the DomainEvents class. Using this attribute, each thread gets its own collection, meaning that handlers registered on one thread are not visible to another thread. Listing 18-14 shows how to use the ThreadStatic attribute with domain events.
然而,在 ASP.NET Web 应用程序中使用这种方法时,存在一些值得关注的问题。Scott Hanselman 在他的博客 ( http://www.hanselman.com/blog/ATaleOfTwoTechniquesTheThreadStaticAttributeAndSystemWebHttpContextCurrentItems.aspx )中解释说, ThreadStatic实际上不应该在 ASP.NET 应用程序中使用。
However, there is cause for concern when using this approach in ASP.NET web applications. Scott Hanselman explains on his blog (http://www.hanselman.com/blog/ATaleOfTwoTechniquesTheThreadStaticAttributeAndSystemWebHttpContextCurrentItems.aspx) that ThreadStatic should, in fact, never be used in ASP.NET applications.
如果您不准备将域与静态事件发布器耦合(许多 DDD 从业者都没有这样做),那么您可以将其作为域模型中方法的参数提供。当您不想出现前面提到的与ThreadStatic属性相关的问题时,这种模式也是一个很好的选择。清单 18-15 展示了一个事件调度器的实例,该实例被传递到域方法中并被调用。
If you’re not prepared to couple your domain to a static event publisher, as many DDD practitioners aren’t, you can instead supply one as an argument to methods in your domain model. This pattern is also a good consideration when you don’t want the previously mentioned problems associated with the ThreadStatic attribute. Listing 18-15 shows an instance of an event dispatcher being passed into a domain method and invoked.
领域事件模式的另一种做法是将事件的发布和处理解耦,从而隔离副作用。这种方法的实现方式是将事件集合存储在聚合根上,并在聚合根完成其任务后发布这些事件。值得注意的是,系统会从服务层调用调度程序来发布事件,从而将技术问题排除在领域模型之外。
Another take on the domain events pattern is to decouple the publishing and handling of events, so that side effects are isolated. This approach is implemented by storing a collection of events on the aggregate root and publishing them once the aggregate root has completed its task. Significantly, a dispatcher is called from the service layer to publish the events, keeping the technical concern out of the domain model.
清单 18-16 展示了OrderForDelivery实体的另一个版本,它保存了一个事件集合(其RecordedEvents属性)。每次从持久层加载该实体时,该集合都将为空。它只包含在当前事务中发生的事件。
Listing 18-16 shows an alternative version of the OrderForDelivery entity that keeps a collection of events (its RecordedEvents property). Each time the entity is loaded from persistence, this collection will be empty. It only contains events that have occurred inside the current transaction.
当触发ConfirmReceipt行为时,不是发布事件(如清单 18-16 所示),而是将事件添加到OrderForDelivery实体的RecordedEvents集合中,如清单 18-16 所示。请注意,该集合是公共的,以便可以在领域模型之外访问它。
When the ConfirmReceipt behavior is triggered, instead of publishing an event (as per Listing 18-16), an event is added to the OrderForDelivery entity’s RecordedEvents collection, as shown in Listing 18-16. Note how the collection is public so that it can be accessed outside of the domain model.
处理事件产生的任何副作用都不会立即发生,就像简单发布事件一样。一些开发人员认为这是可取的,因为副作用不会与当前方法的执行交错发生。
Any side effects that arise from handling the event will not immediately occur, as is the case when events are simply published. This is seen as desirable by some developers because the side effects aren’t interleaved with the execution of the current method.
当实体完成其逻辑执行后,执行线程将返回到管理业务用例的应用服务。此时,来自实体的事件可以被输入到调度程序中,如清单18-17所示。
When the entity has finished carrying out its logic, the thread of execution will return to the application service that is managing the business use case. It is at this point that the events from the entity can be fed into a dispatcher, as shown in Listing 18-17.
如果不想将事件调度器传递到域对象的构造函数中,也可以使用方法注入。清单 18-14 展示了如何将事件调度器传递到ConfirmReceipt()中,其中域事件记录在调度器上而不是实体上。请注意,调度器不会立即调度事件。相反,它会记录这些事件,并在收到来自应用服务层的指示后立即调度它们,如清单 18-18 所示。
It is also possible to use method injection if you do not want to pass an event dispatcher into the constructor of your domain objects. Listing 18-14 shows how an event dispatcher is passed into ConfirmReceipt(), where domain events are recorded on the dispatcher rather than the entity. Note that the dispatcher will not dispatch the events immediately. Instead, it will record them and will dispatch them once instructed to from the application service layer, as previously shown in Listing 18-18.
显然,将调度器传递给方法会污染领域模型的签名,因此使用此模式时需要格外小心。在大多数情况下,您可能希望将事件存储在实体中,并保留在有明显好处的情况下传递调度器。
Clearly, passing a dispatcher into methods is going to pollute the signature of your domain model, so you’ll want to be extra careful about using this pattern. You may want to store events on an entity in the majority of use cases, and reserve passing around a dispatcher for when there is a clear benefit.
要实现调度器,你可以复制清单 18-18 中的大部分细节,但不需要将该类设为静态。或者,你可以使用 IoC 容器,如下例所示。
To implement a dispatcher you can copy most of the details from Listing 18-18, except you won’t need to make the class static. Or you can use an IoC container, as the next example demonstrates.
如果您正在使用 IoC 容器,则可以将其用作事件调度程序。当您已经在使用 IoC 容器或不想构建自己的事件调度程序时,这可能是一个不错的选择。当您需要 IoC 容器提供的高级功能(包括生命周期管理或基于约定的处理程序注册)时,它也是一个不错的选择。基于约定的处理程序注册如清单 18-19 所示,其中 StructureMap 约定扫描项目中的所有程序集,并自动将每个事件处理程序注册到其关联事件中。但是,对于这种模式,您需要创建一个接口来表示事件处理程序。清单 18-19 显示了正在使用的IHandles<T>接口。
If you are using an IoC container you can use it as an event dispatcher. This can be a good choice when you are already using an IoC container or don’t want to build your own event dispatcher. It can also be a good choice when you want advanced features provided by IoC containers, including lifecycle management, or convention-based handler registrations. Convention-based handler registration is shown in Listing 18-19 where a StructureMap convention scans all assemblies in a project and automatically registers each event handler with its associated event. For this pattern, though, you will need to create an interface to represent event handlers. Listing 18-19 shows an IHandles<T> interface being used.
您可以在清单 18-20 中看到,一旦所有事件处理程序都注册完毕,您就可以使用应用程序服务中的容器来调度当前事务期间记录的所有事件。
You can see in Listing 18-20 how once all your event handlers are registered, you can use a container from within an application service to dispatch all events recorded during the current transaction.
使用领域事件模式的测试代码并不比传统的面向对象代码复杂。有些地方甚至可以简化,因为协作者不需要被模拟。不过,测试模式略有不同,因此本节提供了一个简短的指南和示例。
Testing code that uses the domain events pattern is no more complicated than traditional object-oriented code. In places it can even be simplified because collaborators do not need to be mocked. The testing patterns are a little different, though, so this section provides a short guide with examples.
许多单元测试都希望验证实体或领域服务是否引发了事件来表示发生了更改。在网上外卖店场景中,可以用前面讨论过的DeliveryGuaranteeFailed事件来演示这一点。清单 18-21 展示了一个单元测试,它使用模式的静态DomainEvents版本来验证该事件是否引发。
Many of your unit tests want to verify that an entity or domain service raised an event to signify a change occurring. In the online takeaway store scenario, this could be demonstrated with the previously discussed DeliveryGuaranteeFailed event. Listing 18-21 shows a unit test that validates the event is raised using the static DomainEvents version of the pattern.
如清单 18-21 所示,拥有静态DomainEvents类实际上并不会使测试变得困难。使用领域事件模式时,您不想模拟DomainEvents类,这意味着紧密耦合实际上不是问题。这意味着在单元测试中,您可以通过为其注册一个设置标志的回调来验证是否引发了适当的事件。在清单 18-21 中,此标志是eventWasRaised变量。如果测试结束时eventWasRaised为true ,则显然引发了该事件。
Having a static DomainEvents class doesn’t actually make testing difficult, as Listing 18-21 illustrates. When using the domain events pattern, you don’t want to mock the DomainEvents class, meaning the tight coupling is not really a problem. This means that in a unit test, you can verify the appropriate event was raised by registering a callback for it that sets a flag. In Listing 18-21, this flag is the eventWasRaised variable. If eventWasRaised is true at the end of the test, it’s clear that the event was raised.
以类似的方式,在记录领域事件时,您再次调用所需的领域行为,但要验证事件是否被记录而不是被发布。如清单18-22所示。
In a similar fashion, when recording domain events, you again invoke the desired domain behavior, but instead verify that the event was recorded as opposed to being published. This is shown in Listing 18-22.
使用领域事件时,也可以在应用服务层进行集成测试。事实上,您的测试可能看起来与不使用领域事件时非常相似。这是因为事件和事件处理程序只是实现细节,如清单 18-23 中的示例所示。
Integration testing at the application service-layer level is also possible when using domain events. In fact, your tests probably look very similar to when you’re not using domain events. This is because events and event handlers are just implementation details, as the example in Listing 18-23 clarifies.
清单 18-23 中没有任何地方提到领域事件模式。设置测试数据和服务,实例化并调用ConfirmDeliveryOfOrder应用服务,最后断言 NServiceBus 发布了外部RefundDueToLateDelivery命令,由另一个有界上下文处理。无论是否使用领域事件模式作为领域模型的实现,此测试都是相同的。这表明在使用领域事件时,在应用服务层进行测试不会增加额外的复杂性。令人欣慰的是,这也意味着您可以重构现有的领域模型以使用领域事件模式,而无需更改任何应用服务层测试。而且它们还可以向您保证,您没有破坏任何东西。
Nowhere in Listing 18-23 is there a hint of the domain events pattern. Test data and services are set up, the ConfirmDeliveryOfOrder application service is instantiated and invoked, and finally the assertion is made that NServiceBus published an external RefundDueToLateDelivery command to be handled by another bounded context. This test would be the same whether or not the domain events pattern was used as the implementation for the domain model. This shows that testing at the application service layer adds no additional complexity when using domain events. Comfortingly, it also means that you can refactor existing domain models to use the domain events pattern without having to change any of your application service layer tests. And they can also reassure you that you haven’t broken anything.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for this Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 19 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 19 download and individually named according to the names throughout the chapter.
领域模型的结构由实体和值对象组成,它们代表问题领域中的概念。然而,领域对象之间关系的数量和类型可能会导致在代码中实现领域模型时产生复杂性和混乱。不支持行为且仅为更好地反映现实而存在的关联会给领域模型增加不必要的复杂性。可沿多个方向遍历的关联也会增加复杂性。这就是为什么设计领域对象之间的关系与设计领域对象本身同等重要的原因。
A domain model’s structure is composed of entities and value objects that represent concepts in the problem domain. It is, however, the number and type of relationships between domain objects that can cause complexity and confusion when implementing a domain model in code. Associations that do not support behavior and exist only to better reflect reality add unnecessary complexity to a domain model. Associations that can be traversed in more than one direction also increase complexity. This is why designing relationships between domain objects is equally as important as designing the domain objects themselves.
即使模型中的所有关联都是合理的,大型模型仍然存在技术挑战,难以选择既能反映问题域又能表现良好的事务和一致性边界。
Even when all associations in a model are justified, a large model still has technical challenges, making it difficult to choose transactional and consistency boundaries that both reflect the problem domain and perform well.
本章介绍了如何保持领域对象之间的关系简单并与领域不变量保持一致的最佳实践。此外,本章还介绍了领域驱动设计(DDD) 中聚合的概念——一种一致性边界,它将大型模型分解为更小的领域对象集群,从而在技术上更易于管理。这两个主题都侧重于帮助您管理领域模型中的复杂性。
This chapter covers best practices for keeping the relationships between domain objects simple and aligned with domain invariants. It also introduces the Domain-Driven Design (DDD) concept of an aggregate—a consistency boundary that decomposes large models into smaller clusters of domain objects that are technically easier to manage. Both of these topics focus on helping you manage complexity within your domain models.
在设计领域模型时,聚合是一种极其重要的模式。它能帮助你管理技术复杂性,并增加更高级别的抽象,从而简化关于领域模型的讨论和推理。
Aggregates are an extremely important pattern at your disposal when designing a domain model. They help you manage technical complexity, and they add a higher level of abstraction that can simplify talking and reasoning about the domain model.
在模型设计的早期阶段,DDD 新手往往专注于领域实体和值对象,而很少关注领域对象之间的关系。他们通常会默认使用反映真实情况的关联,甚至更糟的是,默认使用反映底层数据模型的关联。如果不合理地解释每个关联并确保其符合领域不变量,就会导致领域模型隐藏重要概念,使领域专家和开发人员都感到困惑,并且在技术上难以实现。
During the early stages of designing a model, novice DDD practitioners tend to focus on the entities and value objects of the domain, paying little attention to the relationships between the domain objects. They often default to associations that mirror real life or, even worse, the underlying data model. The result of not justifying each association and ensuring it matches a domain invariant is a domain model that hides important concepts, confuses both domain experts and developers, and is technically challenging to implement.
依赖关系可能会变得难以承受,尤其是在存在多对多关系的情况下。务必提醒自己,领域模型不同于数据模型。而且,或许同样重要的是,领域模型的目的是支持不变量和用例,而不是用户界面。
Dependencies can become overwhelming, especially when you have many-to-many relationships. It’s important to remind yourself that the domain model is not the same as the data model. And, perhaps just as important, the purpose of the domain model is to support invariants and uses cases rather than user interfaces.
避免复杂的对象图并不难。您可以通过仅允许单向关系来轻松减少实体和值对象之间的关联数量。为了减少关系的数量,您可以证明其包含的合理性。如果关系不是必需的(无法满足不变量),请不要实现它。基本上,不要模拟现实生活。这种关系可能存在于问题域中,但它存在于您的代码中可能不会带来任何好处。
Avoiding complex object graphs doesn’t have to be difficult. You can easily reduce the number of associations between entities and value objects by only allowing relationships in a single direction. To decrease the number of relationships, you can justify their inclusion. If the relationship is not required (does not work to fulfill an invariant), don’t implement it. Basically, don’t model real life. The relationship may exist in the problem domain, but it may not provide a benefit by existing in your code.
总而言之,根据用例需求而非实际需求对领域对象关联进行建模,可以确保更简单的领域模型和更高性能的系统。领域对象之间的关联是为了支持不变量,而不是用户界面问题。简化对象图中的关联。从单一遍历方向的角度对关系进行建模。简化模型,将使其在代码中更易于实现和维护。
To summarize, modeling domain object associations based on the needs of use cases and not real life can ensure a simpler domain model and a more performant system. Associations between domain objects are there to support invariants, not user interface concerns. Simplify associations in object graphs. Model relationships from the point of view of a single traversal direction. Simplify the model, and you will make it easier to implement and maintain in code.
为反映现实而构建的模型将包含许多双向对象关系。这意味着两个对象包含彼此的引用。以采购系统为例,如图 19.1所示。您可以在Supplier和PurchaseOrders之间双向遍历。同样,您可以从Supplier遍历到他们可以供应的所有Product,再从Product遍历回提供该产品的Supplier集合。您还可以从PurchaseOrder遍历到它包含的Product列表,从Product遍历到针对它提出的采购订单列表。ORM 使得在代码中创建这些类型的双向关系变得太容易了,这可能导致深层对象图和运行时性能下降。
A model that is built to reflect reality will contain many bidirectional object relationships. This means that two objects contain a reference to each other. Take the example of a procurement system, as shown in Figure 19.1. You can traverse between Supplier and PurchaseOrders in both directions. Similarly, you can traverse from a Supplier to all the Products that they can supply and back from a Product to the collection of Suppliers that it is available from. You also can traverse from a PurchaseOrder to the list of Products that it contains and from a Product to the list of purchase orders that have been raised against it. ORMs have made it too easy to create these types of bidirectional relationships in code, which can lead to deep object graphs and degraded runtime performance.
图 19.1导致复杂性的双向关系。
FIGURE 19.1 Complexity-causing bidirectional relationships.
如果只想更改供应商的联系方式,真的有必要加载其所有产品吗?如果产品没有可导航的供应商列表,这有关系吗?如您所见,双向关系增加了技术复杂性并模糊了领域概念。它隐藏了一个尤其重要的领域概念,即关系的所有者。为了简化关系,可以将其限制为单个遍历方向,如图19.2所示。
Is it really necessary to load all the Products a Supplier has when you only want to make a change to its contact details? Does it matter if a Product does not have a navigable list of Suppliers? As you can see, a bidirectional relationship adds technical complexity and obscures domain concepts. An especially important domain concept that it hides is the owner of the relationship. To simplify the relationship, you can constrain it to a single traversal direction, as shown in Figure 19.2.
图 19.2约束双向关联。
FIGURE 19.2 Constraining a bidirectional association.
通过选择实体之间的单向关系,领域模型得到了简化。本质上,双向关系已转换为单向关系。这是一种降低复杂性的基本模式,您可以自由选择。
By selecting a uni-directional relationship between the entities the domain model has been simplified. Essentially, a bidirectional relationship has been converted into a unidirectional relationship. This is a fundamental pattern for reducing complexity that you can be liberal with.
定义对象关系时,最好明确地询问:关联实现的行为是什么?谁需要这种关系才能发挥作用?这将有助于避免创建不必要的双向关系。采购场景是一个简单的示例,但在包含许多双向关联的大型领域模型中,情况很快就会变得极其复杂,尤其是在持久化和检索方面。
When defining object relationships, it is good practice to ensure that you explicitly ask: What is the behavior that an association is fulfilling, and who needs the relationship to function? This will help you avoid creating unnecessary bidirectional relationships. The procurement scenario is a simple example, but in a larger domain model with many bidirectional associations, things can quickly become extremely complicated, especially when it comes to persistence and retrieval.
如果您将关联实现为对象引用以支持领域不变量,并且这些关联是一对多或多对多的,则应该对关联进行限定,以减少需要进行水合的对象数量。请考虑图 19.3。领域对象Contract表示一份手机合同,通话记录代表该合同下的所有通话。通话记录需要了解当前期间有多少免费分钟可用,以便正确向客户计费。对于24个月合同的第23个月的客户来说,将拥有数百条通话数据。加载所有这些通话记录会降低性能。但是,如果所有不变量只需要当前期间的通话记录来计算剩余的免费分钟数,那么加载这些通话记录既低效又没有必要。
If you are implementing associations as object references to support domain invariants and those associations are one- or many-to-many, you should qualify the associations to reduce the number of objects that need to be hydrated. Consider Figure 19.3. The domain object Contract represents a mobile phone contract, and Calls represents all calls made on that contract. Calls is required to understand how many free minutes are available for the current period so that a customer can be billed correctly. Customers on the twenty-third month of a 24-month contract will have many hundreds of call data. Loading all these Calls reduces the performance. However, this is inefficient and unnecessary if all the invariant requires is the calls for the current period to compute any remaining free minutes.
图 19.3合格关联。
FIGURE 19.3 Qualifying associations.
为了证明关联,只能检索当前期间的呼叫,如图19.4所示。
To qualify the association, only the calls for the current period can be retrieved, as highlighted in Figure 19.4.
图 19.4使用来自通用语言 (UL) 的过滤标准限定关联。
FIGURE 19.4 Qualifying associations with filter criteria from the Ubiquitous Language (UL).
为了优化性能,您可以使用以下代码片段在数据访问/数据库级别应用此限定。
To optimize performance, you can apply this qualification at the data access/database level using the following code snippet.
一般来说,关联集合中的项数(基数)越大,技术实现就越复杂。因此,可以通过向集合添加约束来降低基数。在处理集合时,最好明确说明;不要陷入在代码中简单地重新创建数据模型的陷阱。
In general, the greater the number of items in an associated collection—the cardinality—the more complex the technical implementation will become. Therefore, aim for lower cardinality by adding constraints to collections. It’s best to be explicit when it comes to collections; don’t fall into the trap of simply re-creating the data model in code.
正如本书反复强调的那样,领域模型的主要目的是对系统的不变量进行建模,以支持业务用例。因此,领域对象之间的关系应该仅出于行为需求而存在。不支持行为的关系会增加领域模型实现的复杂性。对象引用是经典的向域模型添加不必要的、增加复杂性的关系的示例。
As repeated throughout this book, the primary purpose of your domain model is to model the invariants of your system to support business use cases. Therefore, relationships between domain objects should exist only for behavioral needs. Relationships that do not support behaviors can increase complexity in the implementation of the domain model. Object references are the classic example of adding unnecessary, complexity-increasing relationships to a domain model.
可以理解的是,许多开发人员会自然而然地在代码中用对象引用来建模关系。例如,在现实生活中,一个客户有很多订单,但在应用程序的解决方案空间中,可能不存在要求Customer对象保存属于该客户的所有Order集合的不变量。使用对象引用来建模这种关系(如图 19.5所示)会增加不必要的复杂性。
Understandably, many developers find the natural way to model a relationship in code as an object reference. For example, in real life a customer has many orders, but in the solution space of the application, there may be no invariant that requires a Customer object to hold a collection of all Orders belonging to that customer. Modeling this relationship using object references, as shown in Figure 19.5, adds unnecessary complexity.
图 19.5使用对象引用建模关系会增加复杂性。
FIGURE 19.5 Modeling relationships with object references increases complexity.
因为每个关联都是作为对象引用实现的,所以为了下订单,您可以使用以下代码遍历对象图。
Because every association is implemented as an object reference, to place an order, you could use the following code to traverse the object graph.
这段代码可能很慢,因为对于下订单这个基本用例来说,需要加载一个很大的对象图(客户及其所有现有的订单)。实际上需要做的就是将检索到的客户订单添加到其集合中,并将客户的默认送货地址设置为订单派送地址。但是,许多加载的对象并不涉及该用例。这些问题的出现是因为关系被建模为对象引用,要求对象图中的所有对象都从持久层集体加载。您可能会认为延迟加载可以解决这些问题,但延迟加载会使模型更加复杂;此外,它几乎无法传达如何使用领域对象来实现业务案例。
This code could be slow because a large object graph (the Customer and all its existing Orders) needs to be loaded for the basic use case of placing an order. All that really needs to happen is for the retrieved customer to have an order added to its collection with the default delivery address of the customer set as the order dispatch address. But many of the objects loaded are not involved in the use case. These problems arise because the relationship is modeled as an object reference, requiring all objects in the object graph to be loaded from persistence collectively. You might argue that lazy loading makes the problems go away, but lazy loading can further complicate the model; also, it communicates little of how the domain objects are used to fulfill business cases.
实现关联的另一种方法是存储对象的 ID,并使用应用服务中的存储库来获取用例所需的领域对象。通过使用存储库,可以减少模型中的对象引用,从而降低复杂性。图 19.6展示了更清晰的对象模型,其中没有不必要的对象引用。
The alternative method to implement associations is storing the ID of the object and using a repository within an application service to fetch domain objects that are required for a use case. By using the repository, you can reduce the object references in the model and therefore the complexity. Figure 19.6 shows the clearer object model without the unnecessary object references.
图 19.6使用 ID 而不是对象引用来简化关系。
FIGURE 19.6 Simplified relationships using IDs instead of object references.
通过优先使用 ID 和存储库,您需要在应用服务中进行协调,如清单 19-4 所示。在本例中,对_addressBookRepository.FindBy()的调用是Customer对象本身上的对象引用的替代方法。
By preferring IDs and repositories, you need to carry out the coordination in an application service, as shown in Listing 19-4. In this case, the call to _addressBookRepository.FindBy() is the alternative to having an object reference on the Customer object itself.
您可以通过以下问题来确定对象引用是否必要:关联是否支持特定用例的领域不变量?在客户和订单的示例中,Customer拥有Order,但Order只需要Customer的 ID 即可满足不变量;它不需要可导航的引用。尽量不要草率地模拟实际场景或添加遍历对象引用来匹配数据模型;仅在需要满足不变量要求时才添加对象引用。在所有其他情况下,请优先使用 ID 和存储库来降低领域模型中的耦合度。
You can ascertain if an object reference is necessary by asking the question: Is the association supporting a domain invariant for a specific use case? In the example of customers and orders, a Customer has Orders, but an Order only needs a Customer’s ID to meet invariants; it doesn’t require a navigable reference. Try not to carelessly model real life or add a traversal object reference to match the data model; add object references only when they are necessary to meet the requirements of an invariant. In all other cases, favor IDs and repositories to reduce the coupling in your domain model.
图 19.7可视化了领域模型的常见结构,该模型不必要地对现实生活进行建模,并且包含大量的对象引用。图 19.8 则展示了一个分区模型,该模型的对象间关联较少,更倾向于使用 ID 而不是对象引用。如果您遵循本节中的建议,那么如果您想要一个技术复杂度较低的显式领域模型,图 19.8就是您的目标。在本章的剩余部分,您将看到 DDD 聚合如何帮助您实现这一目标。
Figure 19.7 visualizes the common structure of a domain model that needlessly models real life and contains a proliferation of object references. Alternatively, Figure 19.8 shows a partitioned model with fewer associations between objects, favoring IDs instead of object references. If you follow the advice in this section, Figure 19.8 is what you should be aiming for if you want an explicit domain model with reduced technical complexity. In the remainder of this chapter, you will see how DDD aggregates help you achieve it.
图 19.7具有大量不必要关联的复杂领域模型。
FIGURE 19.7 Complex domain model with an abundance of unnecessary associations.
图 19.8仅基于基本关联的更清晰的领域模型。
FIGURE 19.8 Clearer domain model based only on essential associations.
减少和限制领域对象之间的关系可以简化技术实现,并反映出对领域的更深入的洞察。这对于简化代码管理和概念交流非常有利,但仍需谨慎处理将一起使用的对象分组,以确保系统性能良好且可靠。聚合可以帮助您实现所有这些目标,它指导您围绕领域不变量对对象进行有凝聚力的分组,同时充当一致性和并发性的边界。
Reducing and constraining relationships between domain objects simplifies the technical implementation and reflects a deeper insight into the domain. This is highly desirable for making the code easier to manage and concepts easier to communicate, but there is still a need to judiciously group objects that are used together so that a system performs well and is reliable. Aggregates help you achieve all these goals by guiding you into cohesively grouping objects around domain invariants while also acting as a consistency and concurrency boundary.
聚合是所有战术模式中最强大的,但也是最难正确运用的模式之一。虽然有很多指南和原则可以帮助你构建有效的聚合,但开发人员往往只关注这些规则的实现,而忽略了聚合的真正目的和用途——充当一致性边界。
Aggregates are the most powerful of all tactical patterns, but they are one of the most difficult to get right. There are many guidelines and principles you can lean on to help with the construction of effective aggregates, but often developers focus only on the implementation of these rules and miss the true purpose and use of an aggregate, which is to act as a consistency boundary.
领域不变量是必须始终遵守的语句或规则。如果领域不变量被破坏,则表明您未能准确地对领域进行建模。不变量的一个基本示例是,中标的竞标必须始终在拍卖结束前进行。如果中标的竞标在拍卖结束后进行,则该领域处于无效状态,因为不变量已被破坏,并且领域模型未能正确应用领域规则。
Domain invariants are statements or rules that must always be adhered to. If domain invariants are ever broken, you have failed to accurately model your domain. A basic example of an invariant is that winning auction bids must always be placed before the auction ends. If a winning bid is placed after an auction ends, the domain is in an invalid state because an invariant has been broken and the domain model has failed to correctly apply domain rules.
使用不变量作为聚合的设计启发式方法很有意义,因为不变量通常涉及多个领域对象。当一个不变量所涉及的所有领域对象都位于同一个聚合中时,更容易比较和协调它们,以确保它们的集合状态不违反领域不变量。
Using invariants as a design heuristic for aggregates makes sense because invariants often involve multiple domain objects. When all domain objects involved in an invariant reside within the same aggregate, it is easier to compare and coordinate them to ensure that their collective states do not violate a domain invariant.
在中标场景中,拍卖和中标结果将被建模为不同的对象。如果它们都位于不同的聚合中,则任何聚合都不会同时引用这两个对象,从而无法确保不变量在任何时候都不会被破坏,即使是暂时的。这很糟糕,因为不变量永远不应该被破坏。
In the winning bid scenario, both the auction and the winning bid would be modeled as different objects. If both of them resided within a different aggregate, no single aggregate would have a reference to both objects and thus be able to ensure that at no point was the invariant broken, even temporarily. This is bad because invariants should never be broken.
通过对相关的领域对象进行分组,您可以将它们统称为单个概念——这一特性有助于您更高效地沟通和推理。聚合允许您将一组领域对象统称为单个概念,从而为您的领域模型带来这些抽象优势。例如,您可以将它们统称为“订单”,而不是“订单和订单行”。
By grouping related domain objects, you can refer to them collectively as a single concept—a desirable characteristic that lets you communicate and reason more efficiently. Aggregates afford these benefits of abstraction to your domain model by allowing you to refer to a collection of domain objects as a single concept. Instead of an order and order lines, you can refer to them collectively as an order, for example.
为了确保系统可用性和可靠性,我们迫切需要做出明智的选择,确定哪些数据应该保持一致,以及事务边界应该设在哪里。在应用 DDD 时,这些选择源于对涉及相同业务用例的对象进行分组。这些领域对象的内聚组被称为聚合。
To ensure a system is usable and reliable, there is a strong need to make good choices about which data should be consistent and where transactional boundaries should lie. When applying DDD, these choices arise from grouping objects that are involved in the same business use case(s). These cohesive groups of domain objects are aggregates.
一致性的一个选项是通过将整个领域模型包装在单个事务边界中来实现单个聚合。这样做的问题在于,在协作领域中,当执行多项更改时,完全不相关的更改可能会发生冲突。该问题可能表现为数据库级别的阻塞问题或更新失败(由于悲观并发)。
One option for consistency is to have a single aggregate by wrapping the entire domain model in a single transactional boundary. The problem with this is that in a collaborative domain when many changes are being performed there is the potential for a conflict for changes that are completely unrelated. The problem would likely manifest as blocking issues at the database level or failed updates (due to pessimistic concurrency).
图 19.9演示了由于选择跨过多对象的次优事务边界而可能引发的问题。用户 A 想要向客户记录添加地址,而用户 B 想要更改同一客户订单的状态。虽然没有不变量规定在更新订单时不能更改客户的个人信息,但在这种情况下,如果两个更新操作同时进行,其中一个用户的更改将被阻止或拒绝。
Figure 19.9 demonstrates problems that can arise through choosing suboptimal transactional boundaries that span too many objects. User A wants to add an address to a customer record, whereas user B wants to change the state of the same customer’s order. There are no invariants that state while an order is being updated the personal details of a customer cannot change, but in this scenario if both updates are made at the same time, one of the user’s changes will be blocked or rejected.
图 19.9大型事务边界导致的锁定。
FIGURE 19.9 Locking caused by large transactional boundary.
你可能会认为没有事务边界就能解决所有问题。但这很危险,因为这样可能会出现一致性问题,导致违反域不变性。图 19.10展示了两个不同的用户如何更新同一份订单;一个用户正在将折扣码应用于订单,而另一个用户正在修改订单行。由于没有并发检查,并且更改不是事务性的,因此这两个更改都将导致不变量被破坏。在这种情况下,订单可能已经应用了巨额折扣,但该折扣已不再适用。
You might be tempted to assume that having no transactional boundaries would solve all the problems. That would be dangerous, though, because then consistency problems can arise that cause domain-invariant violations. Figure 19.10 shows how two different users are updating the same order; one is applying a discount code to the order, while another is modifying an order line. Because there is no concurrency check and the change is not transactional, both changes will be made, resulting in a broken invariant. In this case, the order could have a massive discount applied that is no longer applicable.
图 19.10由于缺乏事务边界而导致的数据不一致。
FIGURE 19.10 Inconsistent data arising from lack of transactional boundaries.
为了正确应用一致性和事务边界,DDD 从业者依赖于聚合模式。正如前文所述,聚合是对领域对象的显式分组,旨在支持领域模型的行为和不变量,同时充当一致性和事务边界。聚合将领域对象集群视为一个概念整体,其中没有订单线,只有订单。订单线在订单概念之外不存在或毫无意义。聚合定义了领域对象集群的边界,并在一致性和事务机制方面将其与所有其他领域对象区分开来。
To correctly apply consistency and transactional boundaries, DDD practitioners rely on the aggregate pattern. As alluded to, an aggregate is an explicit grouping of domain objects designed to support the behaviors and invariants of a domain model while acting as a consistency and transactional boundary. An aggregate treats the cluster of domain objects as a conceptual whole in that there are no order lines—only an order. The order lines do not exist or make sense outside the concept of an order. The aggregate defines the boundary of the cluster of domain objects and separates it in terms of consistency and transactional mechanism from all other domain objects outside it.
图 19.11展示了将不变量驱动的聚合设计应用于订单示例的结果。可以看到,客户和订单被视为两个独立的聚合,因为领域中不存在同时涉及它们的不变量。
Figure 19.11 shows the result of applying invariant-driven aggregate design to the order example. You can see the customer and order are treated as two independent aggregates because there are no invariants in the domain that involve both of them.
图 19.11将事务边界与域不变量对齐。
FIGURE 19.11 Aligning transactional boundaries with domain invariants.
为了在领域对象集群中强制一致性,所有交互都需要经过一个称为聚合根的实体(本章后面将详细解释),如图19.12所示。聚合外部的对象无法访问聚合内部的任何对象;这确保了对领域对象的控制,并确保了聚合内部的一致性。
To enforce consistency in the cluster of domain objects, all interaction needs to go through a single entity known as the aggregate root (explained in more detail later in the chapter), as highlighted in Figure 19.12. Objects outside of the aggregates can have no access to any of the internal objects of the aggregate; this ensures control of the domain objects and ensures consistency within the aggregate.
图 19.12在聚合根的帮助下强制一致性。
FIGURE 19.12 Enforcing consistency with help from aggregate roots.
领域对象不能单独检索或持久化。聚合作为一个整体,通过存储库从数据存储中提取并提交到数据存储中。聚合是唯一可以从数据库中持久化和检索的内容。除非纯粹用于报表,否则聚合的任何部分都不能单独从数据存储中提取。这确保了聚合之间的最终一致性。
Domain objects are not retrieved or persisted individually. The aggregate as a whole is pulled from and committed to the datastore via a repository. Aggregates are the only things that can be persisted and retrieved from the database. No parts of the aggregates can be separately pulled from the data store unless it is purely for reporting. This leads to eventual consistency between aggregates.
由于聚合是原子性地持久化和检索的,因此作为一个概念上的整体,跨越两个或多个聚合的规则不会立即保持一致(如果每个事务只修改一个聚合)。相反,该规则将最终保持一致。这是因为被更新的聚合仅在内部确保事务一致性。它不负责更新其一致性边界之外的任何内容。因此,聚合有时会从另一个聚合检索到过时的信息,如图19.13所示。
Because aggregates are persisted and retrieved atomically, as a conceptual whole, a rule that spans two or more aggregates will not be immediately consistent (if only one aggregate is modified per transaction). Instead, the rule will be eventually consistent. This is because the aggregate being updated only ensures transactional consistency internally. It is not responsible for updating anything outside its consistency boundary. Therefore, aggregates sometimes have a stale piece of information they retrieved from another aggregate, as shown in Figure 19.13.
图 19.13聚合最终在外部是一致的。
FIGURE 19.13 Aggregates are eventually consistent externally.
为了演示最终一致的规则,我们来考虑一个忠诚度策略:如果一位顾客在过去一年中消费超过 100 美元,那么她以后的所有购买都可以享受 10% 的折扣。在领域模型中,订单聚合和忠诚度聚合是分开的。下订单时,订单聚合会在事务内部单独更新。此时,忠诚度聚合无法获得客户购买历史记录的一致视图,因为它并非在同一事务中更新。但是,订单聚合可以发布一个事件来表示订单已创建,忠诚度聚合可以订阅该事件。忠诚度对象最终将在处理该事件时更新客户的忠诚度,如图19.14所示。
To demonstrate an eventually consistent rule spanning multiple aggregates, consider a loyalty policy: If a customer has spent more than $100 in the past year, she gets 10% off all further purchases. In the domain model, there are separate order and loyalty aggregates. When an order is placed, the Order aggregate is updated inside a transaction exclusively. At that point, the Loyalty aggregate does not have a consistent view of the customer’s purchase history because it was not updated in the same transaction. However, the Order aggregate can publish an event signalling the order was created, which the Loyalty aggregate can subscribe to. In time the loyalty object will be able to update the customer’s loyalty when it handles the event, as Figure 19.14 shows.
图 19.14最终一致的忠诚度聚合。
FIGURE 19.14 Eventually consistent Loyalty aggregate.
重要的是,在某个时期,顾客的忠诚度并不能准确反映其消费金额。这本身就是一个令人不快的特性,但作为一种权衡,它可以避免您在单笔交易中保存大量商品。
Importantly, there is a period in which a customer’s loyalty does not accurately reflect how much she has spent. This is an undesirable characteristic in isolation, but as a trade-off it prevents you from having to save lots of objects within a single transaction.
由于用户体验缺陷以及聚合不同步可能造成的边缘情况,在设计最终一致的聚合时,获得业务支持至关重要。
It is vital that you get business buy-in when designing eventually consistent aggregates due to user experience drawbacks and edge cases that may occur due to out-of-sync aggregates.
有时,在一个事务中修改多个聚合实际上是一个好的做法。但首先,了解这些准则存在的原因很重要,这样你才能意识到忽略它们的后果。
Sometimes it is actually good practice to modify multiple aggregates within a transaction. But it’s important to understand why the guidelines exist in the first place so that you can be aware of the consequences of ignoring them.
当最终一致性的成本过高时,可以考虑在同一事务中修改两个对象。例外情况通常是当业务部门告诉你客户体验太不令人满意时。你不应该直接接受业务部门的决定,然而,它永远不想接受最终一致性。你应该详细说明不使用最终一致性时所涉及的可扩展性、性能和其他成本,以便企业能够做出明智的、以客户为中心的决策。
When the cost of eventual consistency is too high, it’s acceptable to consider modifying two objects in the same transaction. Exceptional circumstances will usually be when the business tells you that the customer experience will be too unsatisfactory. You shouldn’t just accept the business’s decision, though; it never wants to accept eventual consistency. You should elaborate on the scalability, performance, and other costs involved when not using eventual consistency so that the business can make an informed, customer-focused decision.
另一种可以避免最终一致性的情况是,当复杂性过高时。在本章后面,你将看到,健壮的最终一致性实现通常使用异步的、进程外的工作流,这会增加更多的复杂性和依赖性。
Another time it’s acceptable to avoid eventual consistency is when the complexity is too great. You will see later in this chapter that robust eventually consistent implementations often utilize asynchronous, out-of-process workflows that add more complexity and dependencies.
总而言之,每笔交易保存一个聚合是默认方法。但您应该与业务部门合作,评估每个用例的技术复杂性,如果有值得的优势(例如更好的用户体验),则可以有意识地忽略该指南。
To summarize, saving one aggregate per transaction is the default approach. But you should collaborate with the business, assess the technical complexity of each use case, and consciously ignore the guideline if there is a worthwhile advantage, such as a better user experience.
一般来说,较小的聚合会使系统更快、更可靠,因为传输的数据更少,发生并发冲突的机会也更少。因此,在设计过程中,您应该偏向使用较小的聚合。尝试从小处着手,并证明将每个新概念添加到聚合中的合理性。然而,忠实地建模领域仍然至关重要,因此聚合的大小并非优秀设计的唯一标准。
In general, smaller aggregates make a system faster and more reliable, because less data is being transferred and fewer opportunities for concurrency conflicts arise. Accordingly, you should have a bias for small aggregates during design. Try starting small and justifying the addition of each new concept to the aggregate. However, it is still crucial to faithfully model the domain so the size of an aggregate is not the only criteria of good design.
现在将讨论大聚合设计的一些后果,以便您可以有意识地设计聚合,而不是漫无目的地尝试将其缩小。有时,较大的聚合反而会让您受益,因此了解这些准则何时不适用会很有帮助。
Some consequences of large aggregate design will now be discussed so that you can consciously design aggregates rather than just aimlessly try to make them small. Sometimes you will benefit by having larger aggregates, so understanding when the guidelines don’t apply is useful.
聚合的每个成员都会增加需要从数据库加载和保存的数据量,从而直接影响性能。然而,当聚合跨越数据库中的多个表或文档时,性能确实会受到影响。每个表都需要额外的查询或连接,这肯定会损害查询的本地性能,并可能增加数据库服务器的整体压力。
Each member of an aggregate increases the amount of data that needs to be loaded from and saved to a database, directly affecting performance. Performance can really be harmed, though, when an aggregate spans many tables or documents in a database. Each table requires an additional query or join, which can definitely hurt the local performance of the query and potentially the overall stress on database servers.
诚然,在很多情况下,大型聚合的开销可能微不足道。但在另一个极端,性能下降可能严重到足以对业务造成破坏性影响。通过选择较小的聚合,可以降低出现性能问题的风险。如果您确实需要进行性能优化,较小的聚合意味着可以将对领域模型的损害限制在较小的范围内。
Admittedly, the overhead of big aggregates could be inconsequential in many cases. But at the other extreme, the performance degradation may be severe enough to have a damaging business impact. By aiming for smaller aggregates, you reduce the risk of performance problems. If you do ever need to make performance optimizations, a smaller aggregate means compromises to your domain model can be confined to a smaller area.
大型聚合可能承担多项职责,这意味着它涉及多个业务用例。因此,多个用户对单个聚合进行更改的可能性更大。进一步的后果是,并发冲突的可能性增加,从而降低应用程序的可用性和用户满意度。
A large aggregate is likely to have more than one responsibility, meaning it is involved in multiple business use cases. Subsequently, there is greater opportunity for multiple users to be making changes to a single aggregate. As a further consequence, the likelihood of a concurrency conflict increases, reducing the usability and user satisfaction of your application.
您可以在设计聚合时运用这些知识。您可以量化聚合所涉及的业务用例数量。数量越多,您就越应该质疑聚合的边界,并尝试不同的设计方案。不过,领域一致性同样重要。有时单个用例可能是最佳数量,而某些聚合在多个业务用例中可能确实有必要。
You can use this knowledge when you design your aggregates. You can quantify the number of business use cases an aggregate is involved in. The higher the number, the more you should question your aggregate boundaries and experiment with alternative designs. Again, though, domain alignment is important. Sometimes a single use case might be the optimal number, whereas some aggregates might genuinely be necessary in multiple business use cases.
聚合设计也受到可扩展性问题的影响。较大的聚合可能跨越多个数据库表或文档。这是一种数据库级别的耦合形式,可能会阻止您重新定位或重新分区对可扩展性有负面影响的数据子集。
Aggregate design is also influenced by scalability concerns. Larger aggregates may span many database tables or documents. This is a form of coupling at the database level, which may prevent you from relocating or repartitioning subsets of the data that have a negative impact on scalability.
使用以领域为中心的聚合,数据之间的依赖关系会更少。这使您能够以更精细的方式重构或迁移数据。例如,如果您正在开发一个电子商务系统,并且只想将订单数据迁移到另一个数据库中,那么与重新划分聚合以避免订单数据与其他类型的数据(例如客户地址或忠诚度)不必要地耦合相比,这样做会更加顺畅。
With domain-focused aggregates, there are fewer dependencies between your data. This enables you to refactor or relocate data on a more granular basis. For example, if you were working on an e-commerce system and wanted to move just the order data into a different database, there would be less friction than if you had to repartition your aggregates so that order data was not unnecessarily coupled to other kinds of data, such as customer addresses or loyalty.
在网上,你可以找到很多公司将部分数据迁移到不同数据库的案例。事实上,这种趋势被称为“多语言持久化”。一个引人注目的例子是英国广播巨头 Sky,该公司决定将其在线结账数据的存储从 MySQL 迁移到 Cassandra(http://www.computerworlduk.com/in-depth/applications/3474411/sky-swaps-oracle-for-cassandra-to-reduce-online-shopping-errors/),以应对快速获取新在线客户过程中出现的严重性能下降。
You can find many stories online of companies relocating parts of their data into different databases. In fact, this trend has been coined polyglot persistence. One high-profile example is British broadcasting giant Sky, which decided to move storage of its online checkout data from MySQL to Cassandra (http://www.computerworlduk.com/in-depth/applications/3474411/sky-swaps-oracle-for-cassandra-to-reduce-online-shopping-errors/) to combat severe performance degradations as it rapidly acquired new online customers.
在许多情况下,每个有界上下文都有自己的数据存储(参见第 11 章“有界上下文集成简介”),这为可扩展性奠定了坚实的基础。但在某些情况下,在一个有界上下文中可能会出现热点;采用较小的聚合设计可能会让您更好地处理这些问题。
In many cases, each bounded context having its own datastore (see Chapter 11, “Introduction to Bounded Context Integration”) makes for a solid foundation for scalability. But in some cases, you may have hotspots within a bounded context; favoring smaller aggregate design may put you in good shape to deal with them.
聚合的边界决定了哪些对象彼此之间保持一致,以及哪些领域不变量易于执行。这可以说是设计聚合时最重要的方面。本节包含一些原则,可以帮助您确定哪些对象应该分组为聚合。
An aggregate’s boundary determines which objects will be consistent with each other and which domain invariants will be easy to enforce. It is arguably the most important aspect of designing an aggregate. This section contains a number of principles that can help you decide which objects should be grouped as an aggregate.
为了提供一个定义聚合的现实示例,本节将介绍一个虚构的应用程序,该应用程序基于一个类似于 eBay 的在线拍卖网站,名为 eBidder。图 19.15展示了已为解空间定义的有界上下文。
To provide a realistic example of defining aggregates, in this section you follow a fictitious application that is based on the domain of an online auction site, somewhat like eBay, called eBidder. Figure 19.15 shows the bounded contexts that have been defined for the solution space.
图 19.15 eBidder 有界上下文。
FIGURE 19.15 eBidder bounded contexts.
图 19.16列出有界上下文聚合。
FIGURE 19.16 Listing bounded context aggregates.
列表 (Listing) 限界上下文处理待售商品及其销售形式,无论是拍卖还是固定价格。争议 (Disputes) 限界上下文涵盖卖家和会员之间就列表产生的任何争议。会员 (Membership) 限界上下文涵盖 eBidder 网站的会员资格。最后,销售账户 (Selling Account) 限界上下文负责处理费用和销售活动。本节仅使用列表 (Listing) 限界上下文来演示聚合设计原则。
The Listing bounded context deals with the items for sale and the format that they are being sold in, whether that is an auction or a fixed price. The Disputes bounded context covers any disputes raised between a seller and member over a listing. The Membership bounded context covers membership to the eBidder site. Finally, the Selling Account bounded context looks after fees and selling activities. In this section, just the Listing bounded context is used to demonstrate aggregate design principles.
图 19.16展示了 Listing 限界上下文的领域模型。该应用程序的完整源代码可作为本章示例代码的一部分获取。
Figure 19.16 shows the domain model for the Listing bounded context. The full source code for this application is available as part of this chapter’s sample code.
Listing实体的一个实例表示卖家正在出售的一件商品。售出后,该商品可以通过多种运输方式发货,可以按类别列出,并接受多种付款方式。会员也可以对商品列表提出疑问;卖家可以解答这些问题。商品列表具有拍卖等销售形式,拍卖可以接受多位会员的竞价。在任何时候,拍卖中,只要有第一个出价,就一定会有一个中标者。最后,会员可以查看不同的商品列表,而无需出价。
An instance of the Listing entity represents an item being sold by a seller. Once sold, it can be dispatched via a number of shipping methods, can be listed in categories, and can accept many forms of payment method. A listing can also have questions raised about it from members; these questions can have answers from the seller. A listing has a selling format such as an auction, and an auction can have many members bid on it. At any time, an auction will always have a winning bid after the first bid has been placed. Finally, a member can watch different listings without bidding.
创建聚合的最基本规则是,领域对象集群必须基于领域不变量。本章前面已经提到过这一点,本文将进一步详细阐述。
The most fundamental rule for creating an aggregate is that the cluster of domain objects must be based on domain invariants. This was touched on earlier in the chapter and will be demonstrated in further detail here.
定义聚合时,您需要识别那些协同工作且彼此必须保持一致的对象,才能满足业务用例。以下是一些领域规则,它们可以作为决定哪些对象集群应该在列表有界上下文中作为聚合的基础:
When defining aggregates, you need to identify objects that work together and must be consistent with each other to fulfill business use cases. Here are some of the domain rules that can form the basis of deciding which clusters of objects should be aggregates in the Listing bounded context:
可以向清单提出问题;但是,除了可以通过 ID 属性实现的引用之外,没有任何不变量需要同时包含问题和清单的数据。清单作为一个概念,可以独立于问题而存在,并且问题不依赖于除答案之外的任何其他领域对象。因此,可以围绕问题和答案定义一个聚合边界,如图19.17所示。
Questions can be asked of a listing; however, there are no invariants that requires data from both questions and listings apart from a reference that can be implemented via an ID property. A listing as a concept can exist without a question, and a question does not depend on any other domain objects apart from an answer. Therefore, an aggregate boundary can be defined around questions and answers, as shown in Figure 19.17.
图 19.17问题聚合边界定义。
FIGURE 19.17 Question aggregate boundary definition.
拍卖代表了列表的格式。它包含开始和结束日期的数据以及当前中标价,包括会员可以出价的最高金额。要进行竞价,拍卖必须处于活跃状态,并且中标价必须低于预期出价。拍卖不依赖于列表的详细信息来发挥作用。利用这些信息,可以围绕拍卖和中标价域对象定义一个聚合边界,如图19.18所示。
An auction represents the format of the listing. It holds data on the start and end dates along with the current winning bid, including the maximum amount that the member would bid up to. For bidding to occur, the auction has to be active, and the value of the winning bid must be less than the intended bid. The auction does not depend on the details of the listing to perform its role. With this information, an aggregate boundary can be defined around the auction and winning bid domain objects, as highlighted in Figure 19.18.
图 19.18拍卖总量边界定义。
FIGURE 19.18 Auction aggregate boundary definition.
商品详情包含所售商品的所有信息,包括商品所属的类别、可使用的付款方式以及支持的配送方式。不变量要求商品详情必须包含配送方式、类别和付款方式。因此,商品详情聚合可以定义成如图 19.19所示。
A listing contains all information on the item being sold, including what category it is in, what payment methods can be used to pay for the item, and which shipping methods are available for the item. An invariant requires a listing to have a shipping method, category, and payment method. Accordingly, the listing aggregate can be defined as shown in Figure 19.19.
图 19.19列出聚合边界定义。
FIGURE 19.19 Listing aggregate boundary definition.
出价是历史事件;因此,它可以作为自己的聚合存在,因为它不涉及任何不变量。会员和卖家只共享各自的标识符,因此他们也可以成为自己的聚合。会员可以观看拍卖,但没有任何不变量,只是一个包含列表 ID 和会员 ID 的容器;它也可以作为自己的聚合,如图19.20所示。
A bid is a historical event; therefore, it can exist as its own aggregate because it is not involved in any invariants. The member and seller only have their identifiers shared so they, too, can become their own aggregates. A member can watch an auction but doesn’t have any invariants and is just a container with the listing ID and member ID; it, too, can be its own aggregate, as illustrated in Figure 19.20.
图 19.20所有聚合边界定义。
FIGURE 19.20 All aggregate boundary definitions.
您应该尝试将聚合边界与事务对齐,因为单个事务中修改的聚合数量越多,发生并发故障的可能性就越大。因此,尽量在每个用例中只修改一个聚合,以保持系统性能。图 19.21展示了拍卖和列表边界如何与事务对齐;每个聚合都在单独的事务中修改。
You should try to align your aggregate boundaries with transactions, because the higher the number of aggregates being modified in a single transaction, the greater the chance of a concurrency failure. Therefore, strive to modify a single aggregate per use case to keep the system performant. Figure 19.21 shows how the auction and listing boundaries are aligned with transactions; each aggregate is modified inside a separate transaction.
图 19.21将聚合与事务边界对齐。
FIGURE 19.21 Aligning aggregates with transactional boundaries.
如果您发现在事务中修改了多个聚合,这可能表明您的聚合边界可以更好地与问题域保持一致。您应该尝试通过与领域专家讨论用例或试验您的模型来获得新的见解。使用后一种方法,看看您是否可以通过使聚合最终一致来解决问题,以便在事务中只需更新一个聚合。建议从业务角度考虑系统中哪些部分可以接受最终一致性,并将其纳入您的聚合设计中。在 eBidder 应用程序中,Auction和WinningBid不能达到最终一致性。这就是为什么它们是同一聚合的成员。
If you find that you are modifying more than one aggregate in a transaction, it may be a sign that your aggregate boundaries can be better aligned with the problem domain. You should try to find new insights by discussing the use case with domain experts or experimenting with your model. With the latter approach, see if you can resolve the issue by making your aggregates eventually consistent so that only one needs to be updated inside the transaction. It is advisable to get a business perspective on what parts of a system may acceptably be eventually consistent and include them in your aggregate design. In the eBidder application, it’s not acceptable for the Auction and WinningBid to be eventually consistent. This is why they are members of the same aggregate.
聚合不应该围绕用户界面进行设计。在商品详情页面上,您可以找到卖家的详细信息、当前拍卖价格以及商品详情。如果根据用户界面需求来定义聚合边界,聚合的大小就会变得过大,如果卖家想修改商品描述,而会员想出价,就会导致锁定。
Aggregates should not be designed around UIs. On a listing’s page, you would find details of the seller, the current auction price, and the item’s details. If the aggregate boundaries were to be defined based on UI needs, the size of the aggregate would become large and would cause locking if a seller wanted to amend a description at the same time as a member wanted to place a bid.
与其创建大型聚合来满足 UI 需求,不如将多个聚合映射到一个包含页面所需所有数据的视图模型上。这通常以应用服务的形式实现:它会多次调用存储库来加载聚合,然后将聚合中的信息映射到视图模型上。
Instead of creating large aggregates to satisfy UIs, it’s common practice to map from multiple aggregates onto a single view model that contains all the data a page needs. This will usually be in the form of an application service making multiple repository calls to load aggregates, and then mapping information from the aggregates onto the view model.
尽管有从多个聚合填充视图模型的指导原则,但这种方法仍然存在一些缺点,在某些情况下应该考虑其他替代方案。一个明显的迹象表明,您可能需要尝试其他解决方案,例如,当您查询三个或更多存储库来填充单个页面时。三次数据库调用可能会导致性能下降和服务器负载过重。在这种情况下,您可能需要考虑 CQRS,其内容将在第 24 章“CQRS:有界上下文的架构”中介绍。
In spite of the guidelines to populate a view model from multiple aggregates, there are still drawbacks to this approach that should make you consider alternatives in some cases. One clear sign that you may need to try a different solution is when you are querying three or more repositories to populate a single page. Three database calls can lead to poor performance and excessive load on your servers. In such a scenario, you may want to consider CQRS, which is covered in Chapter 24, “CQRS: An Architecture of a Bounded Context ”
一个常见的关于聚合的误解是,它们仅仅是其他对象的集合或容器。这可能是一个危险的误解,会导致你的领域模型不够清晰。每当你看到集合或类似容器的概念时,都不应该盲目地认为它是一个聚合。
A common aggregate misconception is that they are merely collections or containers for other objects. This can be a dangerous misconception that results in a lack of clarity in your domain model. Whenever you see a collection or a container-like concept, you shouldn’t blindly assume that it is an aggregate.
在 eBidder 应用程序中,您可能会考虑将拍卖实体的出价集合纳入聚合。这合乎逻辑,因为从概念上讲,拍卖包含出价集合。然而,正如您在上一节中看到的,出价不属于聚合,因为没有适用于这两个概念的领域不变量。这种思维方式会导致对象图复杂、聚合臃肿,并且无法发挥聚合带来的所有优势。
In the eBidder application, you could look at the auction entity and be tempted to bring its collection of bids into the aggregate. This is logical because, conceptually, an auction has a collection of bids. As you saw in the previous section, though, bids don’t belong to an aggregate because there is no domain invariant that applies to both concepts. This type of thinking can lead to complex object graphs, bloated aggregates, and none of the advantages that aggregates bring.
您的聚合不应受数据模型的影响。领域对象之间的关联不同于数据库表关系。数据模型需要表示每个“有-有”关系,以支持引用完整性,并构建用于商业智能和用户界面屏幕的报告。一个列表包含问题,一个列表包含拍卖,但这不需要建模为单个聚合。请记住,聚合代表领域中的概念,而不是项目的容器。列表不需要存在问题或问题集合,也不需要保存此集合以满足任何领域不变量。为什么需要加载所有问题才能添加另一个问题?
Your aggregates should not be influenced by your data model. Associations between domain objects are not the same as database table relationships. Data models need to represent each HAS-A relationship to support referential integrity and build reports for business intelligence and user interface screens. A listing HAS questions, and a listing HAS AN auction, but this does not need to be modeled as a single aggregate. Remember, an aggregate represents a concept in your domain and is not a container for items. A listing does not need a question or a collection of questions to exist, nor does it need to hold this collection to meet any domain invariants. Why would you need to load all the questions to add another?
拍卖和列表略有不同,因为它们之间存在一对一的关系。然而,从用例和领域不变量的角度来看,列表上的所有行为不必与拍卖总量保持一致,因为它们之间没有跨域的不变量。
Auctions and listings are slightly different because there is a one-to-one relationship here. However, from the perspective of use cases and the invariants of the domain, all behaviors on listings need not be consistent with the auction aggregate because there are no invariants that span across them.
当将域对象包含在聚合中时,不要仅仅关注 HAS-A 关系;证明每个分组的合理性并确保每个对象都需要定义聚合的行为,而不仅仅是与聚合相关。
When including domain objects in an aggregate, don’t simply focus on the HAS-A relationship; justify each grouping and ensure that each object is required to define the behavior of the aggregate instead of just being related to the aggregate.
定义聚合边界是一项可逆且持续的活动。无需强迫自己在初次尝试时就做到完美。相反,您应该随着对领域的了解不断加深,不断寻求改进。一个特别有启发性的场景是,在模型中添加新的业务用例。新的用例可能涉及现有实体并揭示新的关系。因此,可能会出现与您现有聚合设计不太契合的新领域不变量。
Defining aggregate boundaries is a reversible and continual activity. There’s no need to put yourself under pressure to get your design perfect at the initial attempt. Instead, you should continually be looking for improvements as you learn more about the domain. One scenario that can be particularly enlightening is the addition of a new business use case to your model. A new use case may involve existing entities and uncover new relationships. Consequently, new domain invariants may arise that don’t fit well with your existing aggregate designs.
技术层面的洞察也会影响您的聚合,尤其是性能。如果您发现保存(或者更确切地说是加载)聚合的性能超出了可接受的范围,这可能表明您的聚合过大。毫无疑问,即使概念上存在可接受的拟合度,重大的性能问题也是重新定义聚合边界的合理理由。然而,性能问题也可能是概念设计欠佳的征兆,因此您可能需要先咨询领域专家或尝试其他设计。
Technical insights can also affect your aggregates—especially performance. If you find that saving—or more likely loading—an aggregate is outside the acceptable performance range, it might be a sign that your aggregate is too large. Undoubtedly, a major performance problem is an acceptable reason to redefine your aggregate boundaries even if conceptually there is an acceptable fit. However, it’s also possible that the performance problem is a symptom of a suboptimal conceptual design, so you may want to engage with domain experts or experiment with alternative designs first.
专注于从业务用例的角度对聚合进行建模。思考一下为了实现一个用例,必须满足哪些不变量。通过这种方法,你不太可能陷入对现实情况进行建模的陷阱。相反,你将拥有以行为为中心的小型聚合。
Focus on modeling aggregates from the perspective of your business use cases. Ask what invariants must be met to fulfill a use case. By taking this approach, you are less likely to fall into the trap of modeling real life. Instead, you will have small behavior-focused aggregates.
清单 19-5 展示了竞价用例。您会注意到,竞价不需要房源信息、房源类别或描述。这就是为什么房源和拍卖集合需要分别建模的原因。
Listen 19-5 shows the use case of placing a bid. You will notice that it doesn’t require information on the listing, its category, or description to place a bid. This is why the listing and auction aggregates were modeled separately.
聚合代表的是解决方案空间中的概念,并不反映现实情况。它们仅仅是抽象概念,用于以最有效的方式解决问题,同时降低技术复杂性。考虑到这一点,请从业务用例的角度(由外而内,并重点关注领域不变量)定义聚合的边界。
Aggregates represent concepts in the solution space; they don’t reflect real life. They are merely abstractions used to solve problems in the most effective way while reducing technical complexity. With that in mind, define the boundaries of your aggregates from the point of view of your business use cases—outside in and with a focus on the domain invariants.
聚合设计是一个持续的过程,会受到实现反馈的影响。持久性、一致性和并发性都是重要的实现细节,它们可能难以正确处理,并可能导致您重新思考聚合边界。理解聚合之间允许哪些引用尤其难以记住,并且可能会反馈到您的设计过程中。这时,聚合根的概念可以为您提供指导。
Aggregate design is a continuous process that is influenced by feedback from your implementation. Persistence, consistency, and concurrency are all important implementation details that can be tricky to get right and may cause you to rethink your aggregate boundaries. Understanding what references are allowed between aggregates is particularly tricky to remember and is likely to feed back into your design process. This is where the concept of an aggregate root can guide you.
为了使聚合保持一致,其组成部分不应在整个领域模型中共享,也不应该被服务层访问。遵循此准则可以防止应用程序的其他部分将聚合置于不一致的状态。但聚合仍然需要以某种方式提供行为。答案是为每个聚合选择一个实体作为其聚合根。这样,与聚合的所有通信都应该仅通过其根进行。
For an aggregate to remain consistent, its constituent parts should not be shared throughout the domain model or made accessible to the service layer. Following this guideline prevents other parts of an application from putting an aggregate into an inconsistent state. But an aggregate still needs to provide behavior somehow. The answer is to choose an entity for each aggregate to be its aggregate root. All communication with an aggregate should then occur only via its root.
聚合根是被选为聚合网关的实体。图 19.22说明了拍卖实体如何成为拍卖聚合的根。
An aggregate root is an entity that has been chosen as the gateway into the aggregate. Figure 19.22 illustrates how the Auction entity is the root of the auction aggregate.
图 19.22聚合根是进入聚合的门户。
FIGURE 19.22 Aggregate roots are the gateway into an aggregate.
聚合根负责协调聚合的所有变更,确保客户端无法将聚合置于不一致的状态。它通过将聚合集群中的其他实体和值对象委托给聚合根,来维护聚合的所有不变量。
An aggregate root coordinates all changes to the aggregate, ensuring that clients cannot put the aggregate into an inconsistent state. It upholds all invariants of the aggregate by delegating to other entities and value objects in the aggregate cluster.
领域对象仅作为聚合的一部分、作为概念整体的一部分而存在。如前所述,如果没有根,客户端将能够访问聚合的内部结构,能够绕过行为,并直接与成员实体交互,如以下代码片段所示:
Domain objects only exist as part of an aggregate, as part of a conceptual whole. As previously mentioned, without the root, clients would have access to the internal structure of an aggregate, be able to bypass the behavior, and interact with member entities directly, as can be seen in the following code snippet:
basket.Items.Find(x => x.ProductId == productId).Quantity = newQuantity;
basket.Items.Find(x => x.ProductId == productId).Quantity = newQuantity;
此示例中违反的不变量是,订单中每件商品的最大数量为 10。通过将行为移至根,如以下代码片段所示,可以封装聚合的内部成员,以保护聚合的不变量。
The invariant being broken in this example is that an order may have a maximum quantity of ten for each item. By moving the behavior to the root, as shown in the following code snippet, the aggregate’s internal members can be encapsulated to protect the invariants of the aggregate.
basket.ChangeQuantityOf(productId, newQuantity);
basket.ChangeQuantityOf(productId, newQuantity);
ChangeQuantity()函数体中的逻辑禁止Customer或聚合中的任何其他客户端将任何商品的数量增加到 10 以上。这样做,根节点就履行了强制执行不变量的职责。
Within the body of ChangeQuantity() is logic that prohibits a Customer, or any other client of the aggregate, from increasing the quantity of any item to greater than ten. In doing so, the root is carrying out its duty of enforcing the invariant.
与实体和其他领域对象一样,公开聚合的行为非常重要,这样您的模型才能明确地传达领域概念。对于聚合而言,这意味着在根节点上公开一些表达性方法,以便其他聚合与之交互。聚合根节点在聚合的其他成员之间起中介作用,因此是所有外部通信的入口点。以下代码片段中显示的拍卖聚合框架演示了这些特性。
As with entities and other domain objects, it’s highly desirable to expose an aggregate’s behavior so that your model explicitly communicates domain concepts. For an aggregate, this means exposing expressive methods on the root for other aggregates to interact with. An aggregate root mediates between other members of an aggregate and is thus the entry point for all external communication. These characteristics are demonstrated by the skeleton of the auction aggregate shown in the following snippet.
ReduceTheStartingPrice()、CanPlaceBid()和PlaceBidFor()是聚合根(因此也是聚合本身)暴露的行为。这些方法表达了领域概念,并操作聚合的其他成员,但不会暴露它们。如前所述,这使得Auction聚合根能够确保整个聚合始终保持一致。在此示例中,聚合的其他成员是对StartingPrice值对象和WinningBid值对象的封装引用。
ReduceTheStartingPrice(), CanPlaceBid(), and PlaceBidFor() are the behaviors exposed by the aggregate root, and therefore the aggregate. These methods express domain concepts and operate on other members of the aggregate but do not expose them. As discussed previously, this enables the Auction aggregate root to ensure that the entire aggregate is always consistent. In this example, the other members of the aggregate are the encapsulated references to the StartingPrice value object and the WinningBid value object.
并非所有聚合成员都可以直接从根访问。将它们置于对象图的下一级或两级是可以接受的,有时也是一种良好的设计。拍卖聚合中的例子是MaximumBid和CurrentAuctionPrice值对象,它们属于WinningBid 聚合,而不是Auction聚合根,如清单 19-7 中的 WinningBid 框架所示。
Not all members of an aggregate are directly accessible from the root. It’s acceptable, and sometimes good design, for them to be a level or two down the object graph. Examples of this in the auction aggregate are the MaximumBid and CurrentAuctionPrice value objects that belong to the WinningBid and not the Auction aggregate root, as shown by the WinningBid skeleton in Listing 19-7.
您在前几章中了解到,封装领域结构非常重要,因为它可以增强您在对领域知识不断加深的同时重构领域模型的能力。这些建议,例如谨慎使用 getter 和 setter,也非常适用于聚合。如果您遵循上一节中的建议来暴露行为接口,那么您已经朝着保护聚合的内部结构迈出了一大步。
You learned in previous chapters that encapsulating domain structure is important because it enhances the ability to refactor the domain model as your knowledge of the domain improves. Those suggestions, like being wary of getters and setters, are also highly applicable to aggregates. If you follow the advice in the previous section to expose behavioral interfaces, you are already taking a big step toward protecting the internal structure of an aggregate.
如果回顾清单 19-6 中展示的拍卖框架,你会看到一个以行为为中心的聚合根。公共接口仅包含行为。所有对对象内部成员的引用都被封装为私有成员变量。如果聚合暴露了 getter 和 setter,聚合的内部结构可能会暴露。其他领域对象或应用服务可能会与它们耦合,从而损害您在获得新见解时重构领域模型的能力。此外,聚合的外部客户端可能会使其处于不一致的状态。
If you look back to Listing 19-6 showing the Auction skeleton, you see a behavior-focused aggregate root. The public interfaces consist only of behavior. All references to internal members of the objects are encapsulated as private member variables. If an aggregate exposes getters and setters, the internals of the aggregate may be exposed. Other domain objects or application services might then become coupled to them, damaging your ability to refactor the domain model as you gain new insights. Further, external clients of the aggregate may then be able to put it into an inconsistent state.
你可能听到过 DDD 从业者提到局部标识和全局标识。这只是一种简洁的表达方式:聚合根具有全局标识,因为它可以从聚合外部访问;而聚合的其他成员具有局部标识,因为它们位于聚合内部。图 19.23对此进行了可视化。
You may hear DDD practitioners referring to local and global identity. That’s just a concise way of expressing that an aggregate root has a global identity because it can be accessed from outside the aggregate, whereas other members of an aggregate have a local identity because they are internal to the aggregate. Figure 19.23 visualizes this.
图 19.23聚合根具有全局身份。
FIGURE 19.23 Aggregate roots have global identity.
聚合根几乎总是应该保留对另一个聚合根 ID 的引用,而不是对对象本身的引用。这是一个重要的准则,尤其是在使用 ORM 时,因为从数据库加载一个聚合可能会加载它所引用的所有其他聚合。这种情况可能会导致严重的性能下降和难以调试的延迟加载问题。
Almost always, aggregate roots should keep a reference to the ID of another aggregate root and not a reference to the object itself. This is an important guideline, especially when using ORMs, because loading one aggregate from the database may load all the others that it holds a reference to. Such an occurrence can cause critical performance degradation and hard-to-debug lazy loading issues.
在 eBidder 应用的 Listing 限界上下文中,拍卖聚合包含对正在拍卖的Listing ID 的引用,如以下代码片段所示。这是因为Listing不属于拍卖聚合,而对象引用可能会导致前面讨论过的持久化问题。
In the Listing bounded context of the eBidder application, the auction aggregate contains a reference to the ID of the Listing being auctioned, as the following snippet shows. This is because the Listing is not part of the Auction aggregate, and an object reference can cause the previously discussed persistence problems.
public class Auction : Entity<Guid>
{
...
private Guid ItemId { get; set; }
...
}
public class Auction : Entity<Guid>
{
...
private Guid ItemId { get; set; }
...
}
正如您在本章第一部分所看到的,通过 ID 引用其他聚合根会产生依赖存储库在服务层中按需查找所引用聚合的少量成本,类似于以下代码片段:
As you saw in the first part of this chapter, referencing other aggregate roots by ID incurs the small cost of relying on repositories to carry out an on-demand lookup of the referenced aggregate in the service layer, similar to the following snippet:
var auction = auctionRepository.FindById(auctionId);
var listing = listingRepository.FindById(auction.ItemId);
// carry out business use-case
var auction = auctionRepository.FindById(auctionId);
var listing = listingRepository.FindById(auction.ItemId);
// carry out business use-case
每次额外的存储库调用都可能需要额外往返数据库一次。您可能会认为,与一次往返即可获取所有所需数据的单个查询相比,这并非理想之选。诚然,单独来看,这并非理想之选,但通过在域模型中应用聚合模式,您应该能够看到更高效的数据访问策略为整个系统带来的好处。
Each additional repository call is likely to be an additional round-trip to the database. You might think this is suboptimal compared to a single query that fetches all the required data in a single round-trip. Admittedly, in isolation this is not ideal, but by applying the aggregate pattern across your domain model, you should see benefits to the system as a whole by an overall more efficient data-access strategy.
在某些情况下,您可能仍会进行三到四个存储库调用。如果您担心性能问题,请随时重新评估权衡利弊。也许最好使用一个大型聚合来优化用例。如果其余聚合状况良好,则不太可能出现问题。但是,您可能应该考虑使用 CQRS 或事件溯源。
There might still be cases in which you are making three of four repository calls. If performance is any kind of concern, you should feel free to reevaluate trade-offs. Maybe it would be better to have one large aggregate to optimize the use case. If the rest of your aggregates are in good shape, it’s unlikely to be a problem. However, you might instead want to consider using CQRS or event sourcing.
诚然,关于聚合之间哪些类型的引用是允许的,哪些是不允许的,存在一些微妙之处。本节的剩余部分将通过示例来阐明这些细节。
Admittedly, there are a few subtleties regarding which types of references between aggregates are and aren’t allowed. The remainder of this section shows examples that clarify them.
一条容易记住的规则,也是本章反复提到的规则,就是聚合外部的任何内容都不应持有对其内部成员的引用,如图19.24所示。正如所讨论的,这一点很重要,因为它可以保护聚合的内部结构,并防止聚合陷入不一致的状态。
An easy rule to remember, and one that has been repeated throughout this chapter, is that nothing outside an aggregate should hold a reference to its inner members, as Figure 19.24 illustrates. As discussed, this is important because it protects the inner structure of the aggregate and prevents the aggregate from being put into an inconsistent state.
图 19.24聚合的消费者可能不持有对聚合内部成员的引用。
FIGURE 19.24 Consumers of an aggregate may not hold a reference to the aggregate’s internal members.
聚合的消费者可能只持有对聚合根的引用——除了接下来讨论的特殊情况。
Consumers of the aggregate may only hold a reference to the aggregate root—except for the special case discussed next.
尽管聚合不能包含对另一个聚合的内部成员的引用,但可以保存瞬态引用 - 理想情况下,在单个方法内部使用的引用。
Even though an aggregate cannot contain a reference to an internal member of another aggregate, it is acceptable to hold a transient reference—one that is used, ideally, inside a single method.
图 19.25通过使用内部对象的副本在聚合之间共享信息。
FIGURE 19.25 Sharing information between aggregates by using copies of internal objects.
仔细想想,如果聚合内部成员的瞬时引用保存在临时变量中,那么它不太可能导致持久性问题。它不属于聚合的对象图,因此在加载需要瞬时引用的聚合时,它不会被加载。然而,尽管理论上这是安全的,但在实践中,由于潜在的滥用和耦合,你应该谨慎地分配对象引用。相反,建议优先共享对象的副本或视图,而不是引用,如图 19-25 所示。
If you think about it, a transient reference to the inner member of an aggregate that is held in a temporary variable is not likely to cause persistence issues. It is not part of the aggregate’s object graph, so it is not loaded when the aggregate that requires a transient reference is loaded. However, although in theory this is safe, in practice you should be careful about handing out object references due to the potential for abuse and coupling. Instead, it’s advisable to prefer sharing copies or views of an object rather than a reference as shown in Figure 19-25.
矛盾的是,非聚合根可以持有对其他聚合根的引用,如图19.26所示,拍卖聚合内部的WinningBid持有对另一个聚合根的引用。(这是一种单向关系。)
Paradoxically, it is okay for nonaggregate roots to hold a reference to aggregate roots from other aggregates, as shown in Figure 19.26 where the WinningBid from inside the auction aggregate is holding a reference to the root of another aggregate. (This is a unidirectional relationship.)
图 19.26非聚合根可以保存对其他聚合根的引用。
FIGURE 19.26 Non aggregate roots can hold a reference to roots from other aggregates.
在这种情况下,持有引用具体意味着存储的是 ID,而不是对象引用或指针,如示例 19-8 所示。WinningBid通过存储其 ID 来持有对出价成员的引用。
In this scenario, holding a reference specifically means storing the ID rather than the object reference or pointer, as Listing 19-8 shows. The WinningBid is holding a reference to the Member that placed the bid by storing its ID.
如前所述,整个聚合都是从持久层加载的。因此,如果一个对象存储了对另一个对象的直接引用,则两个聚合都需要从持久层加载。下一节将更详细地介绍这一点。
As previously discussed, entire aggregates are loaded from persistence. So if an object stores a direct reference to another object, both aggregates need to be loaded from persistence. This is covered in more detail in the next section.
只有聚合根可以通过数据库查询直接获取。作为聚合内部组件的领域对象只能通过聚合根访问。每个聚合都有一个与之匹配的存储库,该存储库抽象了底层数据库,并且只允许聚合进行持久化和数据更新。这对于确保满足不变量并保持聚合的一致性至关重要。如果可以通过直接连接数据库来访问聚合的子对象,则很容易受到这些问题的影响。
Only aggregate roots can be obtained directly with database queries. The domain objects that are inner components of the aggregate can be accessed only via the aggregate root. Each aggregate has a matching repository that abstracts the underlying database and that will only allow aggregates to be persisted and hydrated. This is crucial in ensuring that invariants are met and aggregates are kept consistent. You are susceptible to these problems if child objects of an aggregate can be accessed by directly connecting to the database.
图 19.27显示了如果要确保不变量不被破坏并且聚合完全一致,应该和不应该从数据库加载聚合。
Figure 19.27 shows how aggregates should and should not be loaded from a database if you want to ensure that invariants are not broken and aggregates are fully consistent.
图 19.27应从数据库中完全加载聚合以保护其完整性。
FIGURE 19.27 Aggregates should be loaded from a database entirely to protect their integrity.
坚持聚合的粒度可以更容易推理和维护,因为您知道只需要在聚合和存储库之间建立一对一的映射。这也意味着您可以用聚合的方式思考。每当您需要信息或行为时,你只需要知道要加载哪个聚合以及要使用哪个存储库。清单 19-9 展示了AnswerAQuestionService应用服务的简单实现,它只需要关心一个存储库并使用它加载单个聚合。
Persisting at the granularity of aggregates can be easier to reason about and maintain knowing that you only need to have a one-to-one mapping between aggregates and repositories. It also means that you can think in terms of aggregates. Whenever you need information or behavior, you just need to know which aggregate to load and which repository to use. This is exemplified by Listing 19-9 that shows the trivial implementation of the AnswerAQuestionService application service that only has to care about one repository and loading a single aggregate with it.
您可以在下一个代码片段中看到IQuestionRepository的接口。请注意,它仅保存和加载整个聚合:
You can see the interface for the IQuestionRepository in the next snippet. Notice how it only saves and loads the entire aggregate:
public interface IQuestionRepository
{
Question FindBy(Guid id);
void Add(Question question);
}
public interface IQuestionRepository
{
Question FindBy(Guid id);
void Add(Question question);
}
您的存储库不仅限于聚合 ID 查找。事实上,除了方法应该保存和加载整个聚合之外,几乎没有其他规则。例如,您可能希望加载某个成员的所有问题,如以下代码片段所示:
Your repositories aren’t limited to aggregate ID lookups. In fact, there are few rules outside the fact that methods should save and load entire aggregates. For instance, you may want to load all questions for a certain member, as the following snippet shows:
public interface IQuestionRepository
{
Question FindBy(Guid id);
void Add(Question question);
IEnumerable<Question> FindByMemberId(Guid memberId);
}
public interface IQuestionRepository
{
Question FindBy(Guid id);
void Add(Question question);
IEnumerable<Question> FindByMemberId(Guid memberId);
}
存储库的实现会根据您选择的数据访问和数据存储技术而有所不同。使用像 NHibernate 这样的 ORM,您通常只需将聚合根传递到会话中,整个聚合就会被持久化。另一方面,使用原始 SQL,您需要手动存储聚合的每个成员。
Implementations of your repositories will vary according to your chosen data access and data storage technologies. With an ORM like NHibernate, you can often just pass the aggregate root into the session and the entire aggregate is persisted. On the other hand, with raw SQL, you need to manually store each member of the aggregate.
您可能会担心,在某些情况下,加载整个聚合效率低下或没有必要,而您只想加载单个领域对象。然而,在应用 DDD 的一些经验之后,您可能会意识到这些情况很少见。以订单行为例。您是否想过只加载单个订单行而不加载订单聚合的其余部分?您可能会用它做什么?需要单个领域对象的场景是例外。更多情况下,它们表示聚合边界不明确。
You may be concerned that there are cases in which loading an entire aggregate is inefficient or unnecessary, when instead you just want to load a single domain object. However, after some experience of applying DDD, you may come to realize that these scenarios are rare. Take an order line, for example. Would you ever want to load a single order line without the rest of the order aggregate? What could you possibly do with it? Scenarios requiring single domain objects are the exception to the rule. More often, they indicate dubious aggregate boundaries.
处理业务用例时,只需从数据库中提取一个聚合即可。然而,对于域状态的报告,您无需担心聚合。报告或查询可以直接在数据库级别执行,而无需对域对象进行水合。相反,可以通过查看特定查询来填充精简视图模型。如图 19.28所示,在执行用例时,聚合应该是从数据库中提取的唯一域概念,但对于报告,对数据库进行简单的查询即可。
When handling business use cases, an aggregate is all that you should pull from the database. For reporting on the state of the domain, however, you need not worry about aggregates. Reporting or querying can be performed directly at the database level without the need to hydrate domain objects. Instead, thin view models can be populated by viewing specific queries. As shown in Figure 19.28, an aggregate should be the only domain concept pulled from the database when fulfilling a use case, but for reporting, a simple query against the database is fine.
图 19.28加载聚合与直接进入数据库。
FIGURE 19.28 Loading aggregates versus going directly to the database.
删除聚合根时,必须同时删除同一事务中的所有子域对象。聚合根被删除后,其组件将无法继续存在,因为如果没有聚合根,它们就失去了上下文和意义。例如,如果订单被删除,订单行就没有任何意义。
When you delete an aggregate root, you must remove all the child domain objects as well within the same transaction. Components of an aggregate root cannot live on after the root is removed because they have no context and no meaning without the root. For example, an order line has no meaning if the order is removed.
许多 ORM 仅允许在代码访问数据库时才从数据库加载数据。虽然这样做的目的是为了提高性能,但这种特性的不可预测性(称为延迟加载)实际上会导致严重的性能和可靠性损失。强烈建议您认真考虑避免使用延迟加载,除非您有极其充分的理由。如果您的聚合函数规模较小,则不太可能需要延迟加载。
Many ORMs allow you to load data from a database only when it is accessed in code. Although the intention is to improve performance, the unpredictability of this feature, known as lazy loading, can actually lead to severe performance and reliability penalties. It is highly recommended that you seriously consider avoiding lazy loading unless you have an extremely good reason. If you keep your aggregates small, you are unlikely to need lazy loading.
延迟加载会损害应用程序性能的众多方式之一是导致可怕的select n+1。本质上,这个问题涉及从数据库中单独检索集合中的每个项目,而不是在单个查询中集体检索。每个额外的查询都需要额外的数据库连接和网络传输开销,导致数据访问效率低下且缓慢。例如,请考虑清单 19-10。
One of the many ways lazy loading can hurt the performance of your application is by causing the dreaded select n+1. Essentially, this problem involves each item in a collection being retrieved individually from the database, rather than collectively in a single query. Each additional query requires an extra database connection and network transportation overhead, leading to inefficient and slow data access. Consider Listing 19-10, for example.
这段代码来自一个特殊活动领域模型,人们可以在此预订冒险日来庆祝生日或公司活动。如果这段代码使用延迟加载,foreach循环的每次迭代都可能导致对数据库进行额外的查询以获取单个客人的详细信息。想象一下,一个办公室有 50 位同事预订圣诞庆祝活动。这可能会产生惊人的 51 次数据库查询(一次查询预订,一次查询每位客人)。如果您能够承受性能损失,这倒不成问题,但在许多系统中,此类查询的低性能会破坏用户体验。
This code is from a special events domain model, where groups of people can book adventure days to celebrate birthdays or corporate events. If this code were using lazy loading, each iteration of the foreach loop might cause an additional query to the database to get the details for a single guest. Imagine a Christmas celebration booking for an office of 50 colleagues. That could be an astonishing 51 database queries (one for the booking, and one for each guest). That’s not a problem if you can afford to give away performance, but in many systems the poor performance of queries like this destroys user experience.
正如您所知,聚合必须在事务内完全持久化或完全回滚才能保持一致性。聚合存储在一张还是多张表中并不重要;当聚合持久化时,需要在单个事务中提交,以确保在发生故障时聚合不会处于不一致的状态。
As you’ve learned, an aggregate must be fully persisted or fully rolled back within a transaction to retain consistency. It doesn’t matter if an aggregate is stored in one table or many; when an aggregate is persisted, it needs to commit in a single transaction to ensure that, in the event of failure, the aggregate isn’t stored in an inconsistent state.
图 19.29展示了拍卖聚合的边界如何成为其一致性边界。拍卖聚合的所有成员都必须以原子方式更新或回滚。
Figure 19.29 shows how the boundary for the auction aggregate is its consistency boundary. All members of the auction aggregate must be updated or rolled back atomically.
图 19.29聚合边界是一致性边界。
FIGURE 19.29 Aggregate boundaries are consistency boundaries.
一致性意味着聚合中的每个成员都能访问聚合中其他成员的最新状态。在拍卖聚合中,这意味着每当WinningBid更新时,拍卖实体必须立即知晓。这条规则在聚合中很容易应用,因为聚合内的对象可以包含彼此的直接对象引用。以下代码片段突出显示了拍卖聚合根是如何实现事务一致性的:
Being consistent means that each member of an aggregate has access to the latest state of other members of the aggregate. In the auction aggregate, this means that whenever WinningBid is updated, the Auction entity must immediately know about it. This rule is easy to apply within an aggregate because objects within an aggregate can contain direct object references to each other. The following snippet highlights that the Auction aggregate root is implemented with transactional consistency:
public class Auction : Entity<Guid>
{
...
private WinningBid WinningBid { get; set; }
...
}
public class Auction : Entity<Guid>
{
...
private WinningBid WinningBid { get; set; }
...
}
每当Auction的WinningBid更新时,Auction都能立即访问更新后的值。这是因为Auction拥有对WinningBid对象的引用。因此,它始终能从数据源获取最新值。
Whenever an Auction’s WinningBid is updated, the Auction immediately has access to the updated value. This is because the Auction has a reference to the WinningBid object. So it always gets the latest value from the source of truth.
在聚合体之外,也就是在一致性边界之外,规则正好相反;一致性不必严格。这是因为对一个不允许聚合。在拍卖示例中,对WinningBid的引用可能不被其他聚合持有。因此,当WinningBid更新时,其他使用该值的聚合或服务很难收到通知。相反,它们必须最终保持一致——必须处理陈旧数据,正如您在本章前面所见。
Outside of an aggregate, and subsequently outside the consistency boundary, the rules are the opposite; consistency does not have to be strict. This is because references to the innards of an aggregate are not allowed. In the Auction example, a reference to WinningBid may not be held by other aggregates. Therefore, when the WinningBid is updated, it is not easy for other aggregates or services that use this value to be notified. Instead, they must be eventually consistent—having to deal with stale data, as you saw earlier in the chapter.
实现事务一致性很大程度上取决于您的持久化技术和客户端库的选择。像 Hibernate 和 NHibernate 这样的 ORM 提供了显式事务,可以提交或回滚其内部发生的所有操作。一些数据库,例如 RavenDB,也提供了类似的接口。
Implementing transactional consistency largely depends on your persistence technology and choice of client library. ORMs like Hibernate and NHibernate offer explicit transactions that commit or roll back everything that happens inside it. Some databases, like RavenDB, provide a similar interface.
从根本上讲,最终一致性是通过聚合将其数据副本分发给其他聚合来实现的,因此,聚合能够处理它们接收到的信息可能过时的情况。这是因为,当一个聚合更新时,其他接收到其状态部分副本的聚合不会立即用新值的副本进行更新。正如您在本章前面所学到的,这就是为什么聚合的边界也是一致性边界的原因。
Fundamentally, eventual consistency is implemented by aggregates handing out copies of their data to other aggregates, and consequently, aggregates being able to deal with the fact that the information they have received may be stale. This is because, when one aggregate is updated, other aggregates that received a partial copy of its state are not updated immediately with a copy of the new value. As you learned earlier in the chapter, this is why an aggregate’s boundary is also a consistency boundary.
实现最终一致性有多种策略。从根本上讲,您可以简单地同步运行事务。这种方法的好处是聚合不一致的时间最短。不建议使用此解决方案,因为它可能存在风险且难以良好实施。实现最终一致性的常用策略是异步方法,使用第 12 章“通过消息传递集成”中介绍的进程外技术,例如 NServiceBus。使用异步方法,聚合会在更长的时间内保持不一致,但实现更可靠。
There are strategies for implementing eventual consistency. At a basic level, you can simply run transactions synchronously. A benefit of this approach is that the period when aggregates are inconsistent is minimal. This solution is not recommended because it can be risky and difficult to implement well. The common strategy for implementing eventual consistency is the asynchronous approach, using an out-of-process technology like NServiceBus that was introduced in Chapter 12, “Integrating via Messaging.” Aggregates stay inconsistent for longer periods of time with the asynchronous approach, but the implementation is more reliable.
正如您在本章前面所了解的,最终一致性通常出现在领域规则跨越多个聚合时。在本例中,您将了解如何为电子商务忠诚度计划实现最终一致性;一年内消费超过 1,000 美元的客户在未来所有购买中均可享受 10% 的折扣。最初,您可能想在同一事务中更新订单和忠诚度聚合。然而,正如您所了解的,这会增加并发冲突的可能性,并减少可扩展性选项。
As you learned earlier in the chapter, eventual consistency usually arises when domain rules span multiple aggregates. In this example, you will see how to implement eventual consistency for an e-commerce loyalty program; customers who spend more than $1,000 in one year get a 10% reduction on all future purchases. Initially, you might be tempted to update the order and loyalty aggregate inside the same transaction. As you’ve learned, though, this increases the chances of a concurrency conflict and reduces scalability options.
忠诚度通常不需要立即实现一致性。作为客户,立即享受 10% 的折扣固然不错,但您可能更愿意等到下一个工作日才生效。记住,在实现最终一致性之前,您应该先达成业务协议。假设利益相关者同意这条规则,那么下一个工作日服务级别协议(SLA),下面的示例演示了如何使用最终一致性和异步域事件来实现它。
Loyalty is often a scenario that does not need immediate consistency. As a customer, it’s nice to have the 10% discount applied immediately, but you’re probably content to wait until the next working day for the discount to come into effect. Remember, before implementing eventual consistency, you should have a business agreement. Assuming that stakeholders agreed this rule could have a next-working-day service level agreement (SLA), the following example demonstrates implementing it using eventual consistency and asynchronous domain events.
异步最终一致性可以提高系统的可靠性,因为操作通常可以在发生故障时重试。然而,其缺点是您需要引入新的技术和服务来执行和管理异步行为。如果您已经在使用 Kafka、Akka 或 NServiceBus 等消息传递技术在有界上下文之间发送消息,那么为聚合实现异步最终一致性的成本就不会那么高。
Asynchronous eventual consistency leads to more reliable systems because operations can usually be retried when failure occurs. However, the drawback is that you need to introduce new technologies and services to carry out and manage the asynchronous behavior. If you’re already using a messaging technology like Kafka, Akka, or NServiceBus for sending messages between bounded contexts, the costs of implementing asynchronous eventual consistency for aggregates will not be as high.
最终,一致性聚合可以采用与第 11 章至第 13 章中实现有界上下文之间最终一致性相同的方式实现——通过使用异步领域事件。这在之前的图 19.14中已经演示过,其中,在单个事务中,更新聚合并发布事件。然后在后续事务中,处理并执行该事件。
Eventually, consistent aggregates can be implemented in the same style that Chapters 11 through 13 implemented eventual consistency between bounded contexts—by using asynchronous domain events. This was illustrated previously in Figure 19.14 where, within a single transaction, an aggregate is updated and an event is published. And then in a subsequent transaction, the event is handled and acted upon.
一种与最终一致性聚合配合良好的模式是使用领域事件模式及其DomainEvents类来触发该过程。在事件处理程序内部,使用消息传递框架发布事件。清单 19-11 展示了更新后的OrderApplicationService使用 NServiceBus 的IBus接口发布一个事件,指示订单已下达。
One pattern that works well with eventually consistent aggregates is using the domain event patterns and its DomainEvents class to trigger the process. Inside an event handler, you publish the event with a messaging framework. Listing 19-11 shows the updated OrderApplicationService using NServiceBus’s IBus interface to publish an event indicating the order has been placed.
在未来的某个时刻,可能在不同的线程甚至不同的服务器上,消息传递框架会调用一个处理程序来运行第二个事务来更新忠诚度。同样,使用 NServiceBus,处理程序类似于清单 19-12。
At some point in the future, probably on a different thread or even a different server, the messaging framework invokes a handler that runs the second transaction to update the loyalty. Again, using NServiceBus, the handler appears similar to Listing 19-12.
使用消息传递框架并非异步解决方案的唯一类型。您在第 11 章“有界上下文集成简介”中了解的另一种选择是数据库集成。在本例中,不是触发消息,而是在数据库中设置一个标志。之后,可以定期运行一个计划作业,扫描所有新订单并更新每个客户的忠诚度,如图19.30所示。
Using a messaging framework isn’t the only type of asynchronous solution. Another option that you learned about in Chapter 11, “Introduction to Bounded Context Integration,” is database integration. In this example, instead of a message being fired, a flag could be set in the database. Later, a scheduled job could run periodically that scans all new orders, and updates the loyalty of each customer, as shown in Figure 19.30.
图 19.30使用数据库集成实现最终一致性。
FIGURE 19.30 Eventual consistency using database integration.
您可能正在思考,异步最终一致性本质上更加复杂,包括消息传递技术、异步工作流和操作监控。建议您根据具体情况决定是要在单个事务中更新多个聚合,还是使用其中一种最终一致性方法。
As you may be thinking, asynchronous eventual consistency has inherently more complexity, including messaging technologies, asynchronous workflows, and operational monitoring. It’s recommended that you decide on a case-by-case basis whether you want to update multiple aggregates in a single transaction or use one of the approaches to eventual consistency.
聚合根可以通过协调对内部组件的访问来保护不变量。然而,它无法防止聚合因多个用户修改而变得不一致。在协作环境中,当多个用户更改业务对象的状态并尝试为了将其同时持久化到数据库中,需要建立一种机制来确保一个用户的修改不会对其他并发用户的事务状态产生负面影响。
An aggregate root can protect invariants by mediating access to internal components. However, it cannot prevent an aggregate from becoming inconsistent due to multiple users modifying it. In collaborative environments, when multiple users change the state of a business object and try to concurrently persist it to the database, a mechanism needs to be in place to ensure that one user’s modification does not negatively affect the state of the transaction for other concurrent users.
并发控制有两种形式:乐观并发和悲观并发。乐观并发假设多个用户同时更改业务对象的状态不会出现问题,这也称为“最后更改生效”。对于某些系统来说,这是完全合理的行为;但是,当业务对象的状态需要与从数据库检索时的状态保持一致时,就需要悲观并发了。
There are two forms of concurrency control: optimistic and pessimistic. The optimistic concurrency option assumes that there are no issues with multiple users making changes simultaneously to the state of business objects, also known as last change wins. For some systems, this is perfectly reasonable behavior; however, when the state of your business objects needs to be consistent with the state when retrieved from the database, pessimistic concurrency is required.
悲观并发有多种形式,从检索记录时锁定数据表,到在更新之前保留业务对象原始内容的副本并将其与数据存储中的版本进行比较。这可确保在事务期间不会由其他方进行任何更改。可以通过在聚合上使用版本/时间戳来实现并发。在本节中,您将使用版本号来检查业务实体自从数据库检索以来是否已被修改。更新时,在提交更改之前,将业务实体的版本号与数据库中的版本号进行比较。如果版本号不匹配,则会引发异常。这可确保业务实体自检索以来未被修改。
Pessimistic concurrency can come in many flavors, from locking the data table when a record is retrieved to keeping a copy of the original contents of a business object and comparing that to the version in the data store before an update is made. This ensures that no changes from other parties are made during a transaction. Concurrency can be implemented by having a version/timestamp on aggregates. In this section, you use a version number to check whether a business entity has been amended since being retrieved from the database. Upon an update, the version number of the business entity is compared to the version number residing in the database before committing a change. If the version numbers don’t match an exception is raised. This ensures that the business entity has not been modified since being retrieved.
在聚合中实现并发支持的常见三步方法是向根添加版本号:
A common three-step approach to implementing concurrency support in an aggregate is to add a version number to the root:
public class Loyalty : Entity<Guid>
{
...
public int Version { get; private set; }
...
}
public class Loyalty : Entity<Guid>
{
...
public int Version { get; private set; }
...
}
然后在事务提交之前增加版本号。但在此之前,请将版本号与最新版本号进行检查。
Then increment the version number before the transaction is committed. But before that, check the version number against the latest version number.
诚然,这种通用的并发方法有时只是一种风险缓解策略。根据数据访问库提供的功能,在检查最新版本号和成功提交事务之间可能仍会存在一小段时间。不幸的是,聚合数据可能会在这段时间内保存在另一个事务中。这就是为什么最好启用数据存储区或持久化库级别的并发检查。许多持久化库,例如 NHibernate、Entity Framework 和 RavenDB 客户端,都可以配置为为您管理并发。
Admittedly, this generic approach to concurrency will sometimes only be a risk mitigation strategy. Depending on features provided by your data-access library, there could still be a tiny period of time between checking for the latest version number and successfully committing the transaction. Unfortunately, it is possible for the aggregate to be saved inside another transaction inside this period of time. This is why it is desirable to enable datastore or persistence-library level concurrency checking. Many persistence libraries, like NHibernate, Entity Framework, and the RavenDB client, can be configured to manage concurrency for you.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 20 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 20 download and individually named according to the names throughout the chapter.
如何在维护不受技术问题干扰的领域模型的同时,创建、持久化和检索领域对象?复杂领域对象的生命周期可能需要协调才能启动和持久化。通过使用“四人帮”工厂模式,可以确保在创建时满足不变量。
How do you create, persist, and retrieve domain objects while maintaining a domain model that is not distracted by technical concerns? The life cycle of complex domain objects may need coordination to begin and when being persisted. Ensuring invariants are met on creation is achieved through the utilization of the Gang of Four factory pattern.
在为大型且丰富的领域创建领域模型时,聚合、实体和值对象可能会变得非常复杂。如果需要深入了解才能确保创建依赖对象的有效实例,这可能会影响领域的表达能力。了解其他对象的不变量会破坏单一责任原则 (SRP)。建议将使用与构造分离,如果创建逻辑复杂或可以更好地表达,则建议将其显式封装在工厂对象中。对象创建并非领域关注的问题,但它确实存在于应用程序的领域层中。您很少会与领域专家谈论工厂,但它们确实发挥着重要作用。
Aggregates, entities, and value objects can become complex when you’re creating a domain model for large and rich domains. If intimate knowledge is required to ensure valid instances of a dependent object are created, it can cloud the expressiveness of the domain. The knowledge of other objects’ invariants breaks the Single Responsibility Principle (SRP). It is recommended that you separate use from construction and explicitly encapsulate creation logic within a factory object if it is complex or if it can be expressed better. Object creation is not a domain concern, but it does live within the domain layer of an application. You will rarely talk about factories to domain experts, but they do play an important role.
您可以使用工厂从持久化模型重构领域对象,也可以使用它们创建新的领域对象,从而封装复杂的创建逻辑。工厂方法模式属于“四人帮”设计模式中的创建型模式,它处理创建对象时无需指定要创建对象的确切类的问题。
You can use factories to reconstitute a domain object from a persistence model, or you can use them to create new domain objects, encapsulating complex creation logic. The factory method pattern belongs to the creational group of the Gang of Four design patterns and handles the issue of creating objects without specifying the exact class of object to be created.
工厂模式的主要目标是隐藏创建对象的复杂性。复杂性可能包括:如果客户端依赖于抽象,则需要决定实例化哪个类;或者,可能需要检查不变量。工厂的第二个目标是清晰地表达对象实例化变体背后的意图,而这通常很难仅使用构造函数来实现。工厂类的标准实现是拥有一个返回抽象类或接口的静态方法。客户端通常(但并非总是)会提供某种信息;然后,工厂会使用所提供的信息来确定要创建并返回哪个子类。抽象出创建子类的责任,使您的客户端代码可以完全不知道依赖类是如何创建的。这遵循了依赖倒置原则。工厂方法模式的另一个好处是,您可以集中创建对象的代码;如果需要更改对象的生成方式,可以轻松找到并更新更改,而不会影响依赖它的代码。
The main objective of the factory pattern is to hide the complexities of creating objects. Complexities can include deciding what class to instantiate if a client depends on an abstraction, or it could be checking invariants. A secondary objective of a factory is to clearly express the intent behind variations of an object instantiation, which is typically hard to achieve using constructors alone. The standard implementation of a factory class is to have a static method that returns an abstract class or interface. The client usually, but not always, supplies some kind of information; using the supplied information, the factory then determines which subclass to create and return. The ability to abstract away the responsibility of creating subclasses allows your client code to be completely ignorant of how dependent classes are created. This follows the Dependency Inversion Principle. Another benefit of the factory method pattern is that you centralize the code for the creation of objects; if a change is required in the way an object is generated, it can be easily located and updated without affecting the code that depends on it.
向聚合添加元素时,重要的是不要暴露聚合的结构。清单 20-1 展示了应用服务如何需要详细了解购物篮才能向其中添加商品。
When you’re adding elements to an aggregate, it’s important not to expose the structure of the aggregate. Listing 20-1 shows how an application service needs to have detailed knowledge of the basket to add items to it.
在清单 20-1 中,应用服务方法需要理解BasketItem构造背后的逻辑。这不应该是它应该承担的责任,因为它应该只关心协调。
In Listing 20-1, the application service method is required to understand the logic behind how a BasketItem is constructed. This is a responsibility it should not have as it should be concerned with coordination only.
通过添加工厂方法,可以避免暴露聚合的内部结构。清单 20-2 展示了带有新 Add 方法的Basket对象,该方法现在向应用服务隐藏了购物篮如何存储商品的实现。您可以看到,责任已经转移,Basket聚合能够确保其内部集合的完整性,因为它可以强制执行不变量。客户端(应用服务)现在变得简单得多,并且不知道Basket如何存储产品。
You can avoid exposing the internals of the aggregate by adding a factory method. Listing 20-2 shows the Basket object with a new Add method that now hides the implementation of how the basket stores items from the application service. You can see that the responsibility has been shifted, with the Basket aggregate able to ensure the integrity of its internal collections because it can enforce invariants. The client, the application service, is much simpler now and has no knowledge of how the Basket stores products.
然而,仍然存在一个问题,即购物篮需要负责创建它不拥有的BasketItem的依赖项(税率)。要从产品中创建有效的商品,购物篮需要提供有效的税率。要创建此税率,它依赖于税率服务。现在,购物篮承担了次要的责任,因为它必须始终了解如何创建有效的商品以及从何处获取有效的税率。
However, there is still the issue of the Basket being responsible for creating dependencies of the BasketItem that it does not own (the tax rate). To create a valid item from a product, the Basket needs to supply a valid tax rate. To create this tax rate, it relies on a tax rate service. The Basket has now taken on a secondary responsibility in that it must always understand how to create a valid item and where to obtain valid tax rates.
为了避免Basket承担过多职责,并隐藏BasketItem的内部结构,可以引入一个工厂对象来封装BasketItem的创建,包括获取正确的税率。清单 20-3 展示了更新后的代码以及它如何委托给BasketItemFactory。如果税率计算方式发生变化,或者BasketItem需要其他类型的信息,Basket类不会受到影响。
To avoid the issue of the Basket being responsible for more than it needs to and to hide the internal structure of the BasketItem, you can introduce a factory object to encapsulate the creation of the BasketItem, including the sourcing of a correct tax rate. Listing 20-3 shows the updated code and how it delegates to the BasketItemFactory. If there are changes to how tax rates are calculated or if the BasketItem needs other types of information, the Basket class is unaffected.
您可以在BasketItem的构造函数中调用该服务,但这不是一个好主意。构造函数应该简洁。如果您发现构造函数中的代码很复杂,这可能表明您需要一个工厂函数。
You could have called the service within the constructor of the BasketItem, but this is not a good idea. Constructors should be simple. If you find you have complex code within a constructor, it may be a sign that you need a factory.
如果有多个选择,并且该选择不是客户端类的责任,那么你也可以在领域层使用工厂来抽象类所需的类型。客户端代码接口或抽象类,如果无法预期正确的类型,则让Factory类负责创建具体类型。
You can also use a factory in the domain layer to abstract the type that a class requires if there are multiple choices and if this choice is not the responsibility of the client class. The client codes against an interface or abstract class and leaves the Factory class responsible for creating the concrete type if the correct type can’t be anticipated.
清单 20-4 展示了订单可以创建寄售;这本身就是一个工厂方法。此方法的有趣之处在于,要创建有效的寄售,必须选择一个Courier。Order类不知道要创建哪个Courier,因此它委托给CourierFactory并针对Courier抽象类进行操作。工厂会创建具体的实现。
Listing 20-4 shows that an order can create consignments; this is itself a factory method. What is interesting in this method is that to create a valid consignment, you must select a Courier. The Order class doesn’t know which Courier to create, so it delegates to a CourierFactory and works against a Courier abstract class. The factory creates the specific implementation.
工厂类本身委托Courier实现上的一个方法来检查它们是否可以处理寄售物品。在每个 Courier 实现的构造函数中,工厂类还会使用相同的方法检查它是否能够满足请求。
The factory class itself delegates to a method on the Courier implementations to check whether they can handle the consignment items. Inside the constructor of each courier implementation, the factory class also checks that it can satisfy the request using the same method internally.
工厂并不总是需要是独立的静态类。工厂方法可以存在于聚合中,以向客户端隐藏对象创建的复杂性。在清单 20-5 中,您之前查看的Basket类现在公开了从BasketItem创建WishListItem的能力。生成的WishListItem对象是WishList聚合的一个实体。在构造之后, Basket与WishListItem没有任何关系,但它确实包含了所需的数据。工厂方法通过将控件移动到Basket中自然适合的位置,消除了客户端(应用服务)了解如何提取BasketItem并将其转换为WishListItem的需要。
Factories don’t always need to be standalone static classes. A factory method can exist on an aggregate to hide the complexities of object creation from clients. In Listing 20-5, the Basket class, which you looked at previously, now exposes the ability to create a WishListItem from a BasketItem. The resulting WishListItem object is an entity for the WishList aggregate. The Basket has nothing to do with the WishListItem after its construction, but it does contain the data required for it. The factory method removes the need for the client, the application service, to know how to extract a BasketItem and turn it into a WishListItem, by moving this control into the Basket where it naturally fits.
工厂方法本身也可以创建聚合。清单 20-6 展示了如果账户中有足够的信用额度,就可以使用Account来创建订单。
A factory method can also create an aggregate itself. Listing 20-6 shows that you can use an Account to create an order if there is enough credit within the account.
如果要绕过信用检查,可以使用另一个工厂方法在Order对象上调用不同的构造函数。如清单 20-7 所示,如果没有工厂方法,就很难理解Order对象多个构造函数背后的意图。
You can have another factory method that calls a different constructor on the Order object if you want to bypass the credit checking. You can see in Listing 20-7 that without the factory methods, it would be difficult to understand the intent behind multiple constructors for the Order object.
如果您没有使用可以通过反射将数据模型直接映射到领域模型的对象关系映射器,或者您正在从遗留系统(无论是通过 Web 服务还是某种平面文件)检索领域对象,则需要重构领域对象,同时确保满足所有不变量。使用工厂重构领域对象比创建对象稍微复杂一些。
If you are not using an object relational mapper that can map a data model to your domain model directly with reflection or if you are retrieving your domain objects from a legacy system, be it via a web service or some kind of flat file, you need to reconstruct your domain objects while ensuring all invariants are met. Using a factory to reconstitute domain objects is slightly more complex than object creation.
清单 20-8 显示,存储库以纯原始数据状态从外部服务检索购物篮持久化模型。然后,存储库委托给领域层中的BasketFactory ,从原始数据创建Basket实例。您可以看到BasketFactory根据原始数据创建了一个DeliveryOption对象。然后,它委托给该对象以确保它可以用于此实例中的购物篮项目。如果DeliveryOption无效,则不使用它;而是使用空对象模式来确保选择新的DeliveryOption 。
Listing 20-8 shows that the repository retrieves the basket persistence model from an external service in a raw data-only state. The repository then delegates to a BasketFactory, which is in the domain layer, to create the Basket instance from the raw data. You can see that the BasketFactory creates a DeliveryOption object based on the raw data. It then delegates to it to ensure that it can be used for the basket items in this instance. If the DeliveryOption is not valid, it is not used; instead, the null object pattern is used to ensure that a new DeliveryOption is chosen.
重构聚合时,满足其不变量至关重要;然而,同样重要的是要认识到对象处于持久化状态的现实情况。在前面的场景中,一种选择可能是抛出异常,并设置某种工作流来修复对象;然而,在这种情况下,构建聚合并用占位符替换交付选项会更容易,这样用户就可以选择合适的交付策略。
When an aggregate is reconstituted, it is vital that its invariants are met; however, it is also important to realize the reality of the situation in that the object exists in the persisted state. An option with the preceding scenario could have been to throw an exception and have some kind of workflow to repair the object; however, in this instance, it was easier to build the aggregate and replace the delivery option with a placeholder so that the user could choose the appropriate strategy for delivery.
工厂方法可以有效地简化领域模型,确保其保持良好的表达能力。然而,它们应该只在有效的情况下使用,而不是在需要实例化对象实例的所有情况下使用。当工厂方法比构造函数更具表达能力,或者当多个构造函数容易混淆时,它能提供便利时,才应该使用工厂方法。当依赖类不关心构造逻辑所需的元素时,也应该使用工厂方法。
Factories can be effective at de-cluttering your domain model to ensure that it remains expressive. However, they should only be used where they are effective and by no means everywhere an instance of an object needs to be instantiated. Use a factory when it is more expressive than a constructor or if it provides convenience where there is the confusion of more than one constructor. Use a factory where elements needed for construction logic are not the concern of the dependent class.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 21 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 21 download and individually named according to the names throughout the chapter.
如何在维护不受技术问题干扰的领域模型的同时,创建、持久化和检索领域对象?对于那些需要在内存中长期使用并稍后检索的领域对象,需要映射到持久化存储。为了避免模糊领域逻辑和基础架构代码之间的责任界限,可以使用存储库。存储库通过将领域聚合存储在集合外观(掩盖真正的底层基础架构机制)之后,确保持久性无知得以保留。存储库确保将技术复杂性排除在领域模型之外。
How do you create, persist, and retrieve domain objects while maintaining a domain model that is not distracted by technical concerns? For domain objects that need to live beyond in- memory use and be retrieved later, a mapping to a persistence store is required. In order to avoid blurring the lines of responsibility between domain logic and infrastructure code, repositories can be employed. Repositories ensure that persistence ignorance is retained by storing domain aggregates behind a collection façade disguising the true underlying infrastructural mechanism. Repositories ensure technical complexities are kept out of the domain model.
存储库用于管理聚合持久化和检索,同时确保领域模型和数据模型之间的分离。它通过使用集合外观来协调这两个模型,从而隐藏底层存储的复杂性。平台和任何持久性框架。存储库提供与 .NET 集合类似的功能,用于存储和删除聚合,但它还可以包含更明确的查询功能,并提供有关聚合集合的摘要信息。
A repository is used to manage aggregate persistence and retrieval while ensuring that there is a separation between the domain model and the data model. It mediates between these two models by using a collection façade that hides the complexities of the underlying storage platform and any persistence framework. The repository provides similar functionality to a .NET collection for storing and removing aggregates, but it can also include more explicit querying functionality and offer summary information on the aggregate collection.
存储库与传统数据访问策略有三点不同:
Repositories differ from traditional data access strategies in three ways:
那么存储库是什么样的?清单 21-1 展示了一个用于管理客户聚合持久化和检索的存储库接口。该接口保存在领域模型命名空间中,因为它是领域模型的一部分,而其实现位于技术基础设施命名空间中。
So what does a repository look like? Listing 21-1 shows an interface for a repository that manages the persistence and retrieval of customer aggregates. The interface is kept within the domain model namespace because it is part of the domain model, with the implementation residing in the technical infrastructure namespace.
存储库的典型客户端是应用服务层。存储库定义了应用服务执行业务任务所需的所有数据访问方法。其实现位于基础架构命名空间下,通常由持久性框架支持以完成繁重的工作。清单 21-2 展示了使用 NHibernate 框架实现的ICustomerRepository 。
A typical client of a repository is the application service layer. A repository defines all the data-access methods that an application service requires to carry out a business task. The implementation comes under the infrastructure namespace and is usually backed by a persistence framework to do the heavy lifting. Listing 21-2 shows an implementation of the ICustomerRepository using the NHibernate framework.
在这种情况下,存储库的实现只是委托给 NHibernate 的ISession接口,该接口充当数据模型的网关。领域对象到数据模型的实际映射是通过 XML、流畅代码或属性映射来处理的。由于 NHibernate 提供 POCO 持久化(无需领域模型对象继承或实现任何类的持久化),因此它可以将领域模型直接映射到数据模型。如果您没有使用过 NHibernate 或对象关系映射器 (ORM),也不用担心;本章末尾的一些实现练习讲解了如何使用 RavenDB、Entity Framework 和 NHibernate 来实现存储库。
The implementation of the repository in this case simply delegates to NHibernate’s ISession interface, which acts as a gateway to the data model. The actual mapping of domain objects to the data model is handled via XML, fluent code, or attribute mapping. Because NHibernate provides POCO persistence (persistence without requiring the domain model objects to inherit or implement any classes), it can map the domain model directly to the data model. Don’t worry if you have no experience with NHibernate or Object Relational Mappers (ORMs); some implementation exercises at the end of this chapter explain how to use RavenDB, Entity Framework, and NHibernate to implement a repository.
需要注意的重点是,存储库隐藏了域模型的技术复杂性,使域对象能够专注于业务概念和逻辑。
The salient point to take away is that repositories hide technical complexity from your domain model, enabling domain objects to focus solely on business concepts and logic.
人们对存储库模式存在很多误解和困惑,许多人认为它是不必要的繁琐程序和不必要的抽象。如果不与丰富的领域模型结合使用,存储库模式会过于复杂,对于更简单的数据访问对象,可以避免使用存储库模式,或者直接使用持久化框架会更好。然而,在为复杂领域建模解决方案时,存储库是模型的扩展。它揭示了聚合检索背后的意图,并且可以采用对领域而非技术框架有意义的方式编写。
There is much misunderstanding and confusion around the repository pattern, with many regarding it as unnecessary ceremony and needless abstraction. When it’s not used in conjunction with a rich domain model, the repository pattern is overly complex and can be avoided for a simpler data access object or better by using a persistence framework directly. However, when modeling a solution for a complex domain, the repository is an extension of the model. It reveals the intent behind aggregate retrieval and can be written in a manner that is meaningful to the domain rather than a technical framework.
许多开发人员在博客中负面地指出,存储库是一种反模式,因为它隐藏了底层持久化框架的功能。这实际上正是存储库的意义所在。存储库并没有提供一个开放的数据模型接口来支持任何查询或修改,而是通过使用命名查询方法并限制对聚合级别的访问,使检索操作变得显式化。通过显式化检索,查询调优变得更加容易,更重要的是,它能够用领域专家能够理解的术语(而不是 SQL)来表达查询意图。除了查询之外,存储库还公开了有意义的持久化方法,而不是盲目地允许所有创建、读取、更新和删除 (CRUD) 方法,而不管每个方法的适用性。
Many developers have negatively blogged that a repository is an antipattern because it hides the capabilities of an underlying persistence framework. This is actually the point of the repository. Instead of offering an open interface into the data model that supports any query or modification, the repository makes retrieval explicit by using named query methods and limiting access to the aggregate level. By making retrieval explicit, it becomes easy to tune queries, and more importantly express the intent of the query in terms a domain expert can understand rather than in SQL. Besides queries, the repository exposes meaningful persistence methods instead of blindly allowing all the create, read, update, and delete (CRUD) methods, regardless of how appropriate each may be.
存储库模式的目的并非简化代码测试,也并非简化底层持久化存储选项的替换。它的目的是将领域模型与技术持久化框架分离,以便模型在演进过程中不受底层技术的影响。如果没有存储库层,持久化基础设施可能会泄漏到领域模型中,从而削弱其完整性,最终影响其实用性。
The point of the repository pattern is not to make it easier to test your code or to make it easy to swap out underlying persistence storage options. It’s to keep your domain model and technical persistence framework separate so your model can evolve without being affected by the underlying technology. Without a repository layer, your persistence infrastructure will likely leak into your domain model and weaken its integrity and ultimately usefulness.
人们对存储库模式的反对似乎源于对其适用场景和原因缺乏理解。它在某些情况下有用,但并非在所有情况下都适用。就像领域模型模式或任何设计模式一样,盲目地将其应用于所有问题只会增加复杂性,而不是减少复杂性。
The backlash against the repository pattern seems to stem from a lack of understanding about where and why it should be used. It is a pattern that is useful in certain circumstances but not all. Just like a domain model pattern or any design pattern, blindly applying it to all problems results in more complexity, not less.
如果您的持久化存储是关系数据库,并且您正在使用 ORM(旨在消除手动将对象映射到行、将属性映射到列以及编写原始 SQL 的需要),您可能会想知道为什么还要费心实现存储库模式,因为您已经有了一个抽象出持久化技术的框架。然而,问题在于 ORM 仅抽象了关系数据模型。它只是以面向对象的方式表示数据模型,使您能够轻松地在代码中操作数据。
If your persistence store is a relational database and you are using an ORM—which is designed to remove the need to manually map objects to rows and properties to columns, and write raw SQL—you might be wondering why to bother implementing the repository pattern because you already have a framework that abstracts away the persistence technology. The issue, however, is that an ORM only abstracts the relational data model. It merely represents the data model in an object-oriented manner, enabling you to manipulate data easily in code.
ORM 映射到的持久性模型与域模型不同,如图 21.1所示。持久性模型是关系数据库中的模型;它由表和列组成,而不是实体和值对象。对于某些域模型,数据模型可能看起来相似,甚至相同,但在概念上它们非常不同。域模型是问题域的抽象,具有丰富的行为和语言。数据模型只是一个存储结构,用于包含域模型在给定时间的状态。ORM 映射到数据模型并使其更容易使用。它们与域模型关系不大。存储库的作用是将两个模型分开,而不是让它们混为一谈。ORM 不是存储库,但存储库可以使用它来持久化域对象的状态。
The persistence model that your ORM maps to is different from your domain model, as shown in Figure 21.1. The persistence model is the one within your relational database; it’s made up of tables and columns, not entities and value objects. For some domain models, the data model may appear similar, or even the same, but conceptually they are very different. Your domain model is an abstraction of your problem domain, rich in behavior and language. The data model is simply a storage structure to contain the state of your domain model at a given time. ORMs map to the data model and make using it easier. They have little to do with domain models. The role of the repository is to keep the two models separate and not to let them blur into one. An ORM is not a repository, but a repository can use it to persist the state of domain objects.
图 21.1域和持久性模型之间的 ORM 映射。
FIGURE 21.1 An ORM maps between the domain and persistence model.
如果你的领域模型与数据模型相似,像 NHibernate 这样的复杂 ORM 或许能够将你的领域对象直接映射到数据存储区。然而,如果不是这样,你最好使用一个完全独立的数据模型,而不是妥协你的领域模型。本章稍后将介绍实现此目的的一些选项。对于文档存储,此问题不存在,并且域模型可以不受任何影响地进行序列化。
If your domain model is similar to your data model, a sophisticated ORM like NHibernate may be able to map your domain objects directly to the datastore. However, if not, you may be best served having a completely separate data model instead of compromising your domain model. This chapter examines some of the options to achieve this a little later. For document stores, this problem doesn’t exist, and the domain model can be serialized without compromise.
将数据模型与领域模型分离的好处是,它允许你改进领域模型,而无需不断思考数据存储和持久化方式。当然,最终还是需要持久化,你可能需要采取务实的态度并做出妥协,但这些妥协应该只在绝对必要时做出,并且是在建模工作完成后,而不是在创建模型之前。
The benefit to separating the data model from the domain model is that it allows you to evolve the domain model without having to constantly think of the data storage and how it will be persisted. Of course, it will ultimately need to be persisted, and you may need to take a pragmatic view and make compromises, but these compromises should be made only when absolutely required, and after the modeling effort is complete rather than up front with the creation of a model.
开发人员喜欢复用代码。他们将一个概念泛化,创建一个可以用于所有变体的通用类,即使单独的显式解决方案可以更好地传达每个概念的意图。您经常会看到在领域模型中定义一种通用存储库契约的形式。清单 21-3 展示了这样一个接口,它包含您需要的所有操作,包括一个开放扩展的查询方法。
Developers love to reuse code. They generalize a concept to create a common class that can be used for all variations, even though separate explicit solutions would convey the intent of each concept far better. Often you see a form of generic repository contract defined within the domain model. Listing 21-3 shows such an interface with all the operations you would need, including an open-to- extension query method.
领域层中定义的存储库契约将继承此接口,类似于清单 21-4。
A repository contract defined in the domain layer would inherit this interface and would resemble Listing 21-4.
此类契约的问题在于,它假设所有聚合都支持相同的行为并具有相同的需求。有些聚合可能是只读的,有些聚合可能不支持 remove 方法。当聚合不支持某个概念时,你通常会发现存储库实现抛出异常,如清单 21-5 所示。
The problem with a contract like this is that it assumes that all your aggregates support the same behavior and have the same needs. Some aggregates may be read only, and some may not support the remove method. When an aggregate does not support a concept, you will often find the repository implementation throwing an exception, as in Listing 21-5.
该接口的另一个问题是FindAllMatching方法的抽象漏洞。通过提供一个开放扩展的方法,您将无法控制查询并优化获取策略。这种对临时查询的支持很快就会导致关系数据库出现严重的性能问题。
Another problem with the interface is the leaky abstraction of the FindAllMatching methods. By providing a method open to extension, you are making it impossible to control queries and optimize fetching strategies. This support of ad hoc queries can quickly lead to severe performance problems in relational databases.
试图将通用策略应用于所有存储库是一个坏主意。它没有说明检索聚合背后的意图。像所有问题一样,最好明确说明。根据存储库的各自需求定义它们,并在命名查询方法时明确说明,如清单 21-6 所示。
Trying to apply a generalizing strategy to all repositories is a bad idea. It says nothing about the intent behind the retrieval of aggregates. Like all problems, it is better to be explicit. Define your repositories based on their individual needs, and be explicit when naming query methods, as shown in Listing 21-6.
在实现方面,通用存储库有其用武之地。在具体实现背后,可以使用通用存储库来避免代码重复。如清单 21-7 所示,这将显式契约与代码重用的好处结合在一起。
When it comes to implementation, there is a place for the generic repository. Behind the concrete implementation, a generic repository can be used to avoid code duplication. As shown in Listing 21-7, this couples an explicit contract with the benefit of code reuse.
如果您正在使用持久性框架,则无需对其进行抽象;您可以在存储库的具体实现中直接使用它。清单 21-8 显示了使用 RavenDB 框架的示例。
If you are using a persistence framework, there is no need to abstract it; you can use this directly within the concrete implementation of a repository. An example using the RavenDB framework is shown in Listing 21-8.
这里你不需要从底层框架中抽象出来,因为这样做没有任何价值。记住,你只是把技术问题排除在领域模型之外。所有其他部分应用程序不需要抽象和隐藏实现细节,因为这种误导只会使代码的读者感到困惑。
Here you don’t abstract away from the underlying framework because there is no value in doing so. Remember that you are only keeping technical concerns out of the domain model. All other parts of the application need not abstract and hide the implementation details, because this misdirection only proves to confuse readers of the code.
您的设计策略、聚合的形状以及您是在全新环境还是现有环境中工作,都会影响您持久化领域对象的方式。然而,您应该首先考虑的是,创建领域对象时,不要考虑持久化需求;这是存储库的工作。您的领域对象应该摆脱任何基础架构代码,并尽可能地使用 POCO(普通的老式 C# 对象)。只有在万不得已的情况下,才应该做出妥协。
Your design strategy, the shape of your aggregates, and whether you are working in a greenfield or a brownfield environment will affect the options for the way you persist your domain objects. The thing that should be at the front of your mind, however, is that you should create your domain objects without thinking about persistence requirements; that is the job of the repository. Your domain objects should be free from any infrastructural code and be as POCO (Plain Old C# Objects) as possible. Compromises should be made only as a last resort.
如果您在绿地环境中映射到关系数据库,并且使用支持持久无知域对象的 ORM,那么您将能够将域模型直接映射到数据模型,如图21.2所示。
If you are mapping to a relational database in a greenfield environment and you are using an ORM that supports persistent ignorant domain objects, you will be able to map your domain model directly to the data model, as shown in Figure 21.2.
图 21.2域和持久性模型之间的 ORM 映射。
FIGURE 21.2 An ORM maps between the domain and the persistence model.
ORM 通常使用反射来持久化你的领域对象,并支持使用私有 getter 和 setter 来持久化完全封装的领域模型。有些 ORM 需要对领域对象进行一些小的修改,例如 NHibernate 中的无参数构造函数,但为了实现从领域到数据模型的无缝映射,这只是一个小小的代价。
An ORM usually uses reflection to persist your domain objects and supports the persistence of a fully encapsulated domain model with private getters and setters. Some ORMs require small additions to your domain objects, such as a parameterless constructor in the case of NHibernate, but this is a small price to pay for a seamless map from the domain to the data model.
如果您正在使用框架可以序列化聚合的文档存储,如图21.3所示,您将能够直接映射聚合,而无需额外映射到数据模型。
If you are using a document store where the framework can serialize your aggregate, as shown in Figure 21.3, you will be able to map the aggregate directly without needing an additional mapping to a data model.
图 21.3聚合可以被序列化和存储。
FIGURE 21.3 An aggregate can be serialized and stored.
一个框架可以让您的领域模型摆脱基础设施的困扰,让您能够毫不妥协地发展您的模型,并且可以对您的建模方式产生重大影响。
A framework that allows your domain model to be free of infrastructural concerns can enable you to evolve your model without compromise and can have a big impact on how you approach modeling.
如果您使用的持久化框架不允许您的领域模型忽略持久化,则需要采用不同的方法来持久化和检索领域对象,以使它们免受基础架构的影响。您可以通过多种方式实现这一点,但这些方式都会影响领域模型和聚合的结构。当然,这是使应用程序正常运行所必需的妥协。
If you are using a persistence framework that does not allow your domain model to be persistence ignorant, you need to take a different approach to the way you persist and retrieve your domain objects so they remain free of infrastructural concerns. There are a number of ways that you can achieve this, but all affect the domain model and the shape of your aggregates. This is, of course, the compromise you need to make your application work.
实现领域对象持久化的一个简单方法是确保所有属性都具有公共的 getter 和 setter。问题在于,这会导致你的领域对象处于无效状态,因为客户端可以绕过你的状态修改方法。你可以通过代码审查和治理来避免这种情况。然而,公开属性确实可以简化领域模型的持久化,因为你的存储库可以轻松访问领域对象的状态。
A simple method to enable the persistence of domain objects is to ensure that all properties have public getters and setters. The problem is that this leaves your domain objects open to being left in an invalid state because clients can bypass your state-altering methods. You can avoid this through code reviews and governance. However, exposing your properties does make it easy to persist your domain model because your repository has easy access to the domain object’s state.
在清单 21-9 中,您可以看到Basket域对象具有其项目集合和配送费用的公共属性。如果客户端绕过Add方法,而是直接将项目添加到集合中,则配送费用不会更新。
In Listing 21-9, you can see that the Basket domain object has public properties for both its collection of items and its delivery cost. If a client bypasses the Add method and instead directly adds an item to the collection, the delivery cost is not updated.
然而,从好的方面来看,存储库实现可以将数据模型与领域模型分离。清单 21-10 展示了一个示例。
On the plus side, however, a repository implementation can keep the data model separate from the domain model. Listing 21-10 shows an example of what this might look like.
在清单 21-10 中,您可以看到数据模型被检索,然后映射到篮子域对象的新实例。这使得域模型完全不知道任何持久性存储,但这是以暴露域模型的属性为代价的。
In Listing 21-10, you can see that the data model is retrieved and then mapped to a new instance of the basket domain object. This enables the domain model to be completely unaware of any persistence store, but this is at the cost of exposing the properties of your domain model.
如果你不想暴露领域模型的属性,并希望它们被完全封装,那么你可以使用备忘录模式。四人帮模式可以让你恢复对象恢复到先前状态。本例中的先前状态可以存储在数据库中。其工作方式是,聚合生成自身的快照,该快照可以持久化。聚合知道如何根据同一快照进行数据融合。重要的是要理解,快照并非数据模型;它仅仅是聚合的状态,同样不受任何持久化框架的约束。存储库仍然需要将快照映射到数据模型。图 21.4展示了此模式的图形表示。
If you don’t want to expose your domain model’s properties and want them to be totally encapsulated, you can utilize the memento pattern. The Gang of Four pattern lets you restore an object to a previous state. The previous state in this instance can be stored in a database. The way in which it works is that an aggregate produces a snapshot of itself that can be persisted. The aggregate would know how to hydrate itself from the same snapshot. It’s important to understand that the snapshot is not the data model; it’s merely the state of the aggregates, which again is free from any persistence framework. The repository would still have to map the snapshot to the data model. Figure 21.4 shows a graphical representation of this pattern.
图 21.4备忘录模式使您能够将域模型的快照映射到持久性模型。
FIGURE 21.4 The memento pattern enables you to map a snapshot of the domain model to the persistence model.
清单 21-11 展示了Basket聚合上的备忘录模式的实现。
Listing 21-11 shows an implementation of the memento pattern on the Basket aggregate.
存储库实现看起来像清单 21-12。
The repository implementation would look like Listing 21-12.
另一种选择是将快照直接映射到数据模型(如果您使用的持久性框架允许这样做但可能不允许映射完整的域对象)。
Another option is to map the snapshot directly to your data model if you are using a persistence framework that allows this but may not allow mapping a full domain object.
持久化领域模型的另一种方法是使用事件流。事件流类似于备忘录模式,但它不是对聚合进行时间快照,而是以领域事件的形式持久化聚合中发生的所有事件。监听来自领域模型的事件,并将其映射到数据模型。同样,你需要一个工厂方法来重构和重放这些事件,从而重建聚合。图 21.5展示了一个图形表示。事件流将在第 22 章中介绍。
Another way to persist your domain model is to use an event stream. Event streams are similar to the memento pattern, but instead of taking a snapshot in time of your aggregate, your repository persists all the events that have occurred to the aggregate in the form of domain events. Listen for events from the domain, and map them to a data model. Again, you need a factory method to reconstruct and replay these events to rebuild the aggregate. Figure 21.5 shows a graphical representation. Event streams are coved in Chapter 22
图 21.5将已发生的事件保存到聚合中。
FIGURE 21.5 Save the events that have occurred to an aggregate.
在所有领域模型持久化策略中,务实是关键。纯领域模型应该对持久化机制一无所知,也就是说,它应该不受任何底层持久化框架需求变更的影响。纯粹性在理论上是好的,但在实践中可能难以实现,有时你必须选择一种务实的方法。
In all the strategies of domain-model persistence, it pays to be pragmatic. A pure domain model should be persistence ignorant in that it should be immune to changes required by the needs of any underlying persistence framework. Purity is good in theory, but in practice it can be difficult to achieve, and sometimes you must choose a pragmatic approach.
如果您的公司有数据存储或框架策略,那么在物理持久化领域对象方面可能需要做出一些妥协。请确保您选择的策略适合框架和可用的数据存储选项。不要与您的框架作对,并认识到您可能需要根据性能和可扩展性来更改领域模型。但是,不要考虑这些事情;从纯粹开始,然后在需要时务实一些。不要让技术框架驱动您的设计;要考虑聚合和显式的数据检索,而不是临时查询。
If you have a corporate policy on datastores or frameworks, you may need to make compromises when it comes to physically persisting your domain objects. Ensure that you choose a strategy that befits the framework and data storage options available. Don’t fight your framework, and recognize that you may need to change your domain model based on performance and scalability. However don’t think about these things; start pure, then be pragamatic when you need to. Don’t let technical framework drive your design; think in aggregates and explicit retrieval of data rather than ad hoc querying.
存储库的契约不仅仅是一个 CRUD 接口。它是领域模型的扩展,并且以领域专家能够理解的术语编写。您的存储库应该根据应用程序用例的需求构建,而不是从类似 CRUD 的数据访问角度构建。应用层是从存储库中提取聚合并将工作委托给它们的客户端。您的领域模型包含契约,但除了领域服务中表示的特定用例外,很少使用它们。
The contract of a repository is more than just a CRUD interface. It is an extension of the domain model and is written in terms that the domain expert understands. Your repository should be built from the needs of the application use cases rather than from a CRUD-like data access standpoint. The application layer is the client that pulls aggregates from the repository and delegates work to them. Your domain model houses the contracts but rarely uses them except for a specific use case represented in a domain service.
存储库并非一个对象。它是一个过程边界和明确的契约,在为其方法命名时,需要付出与领域模型中对象一样多的努力。你的存储库契约应该具体、能够揭示意图,并且对你的领域专家有意义。
The repository is not an object. It is a procedural boundary and an explicit contract that requires just as much effort when naming methods upon it as the objects in your domain model do. Your repository contract should be specific and intention revealing and mean something to your domain experts.
考虑清单 21-13 中的存储库接口。它允许客户端通过任何方式查询领域对象。该契约灵活且易于扩展,但它没有告诉您领域模型中获取聚合集合的检索策略的需求或意图。为了真正理解这些方法是如何使用的,开发人员需要仔细阅读所有代码以了解查询的意图和需求,更不用说如何优化它们了。由于该契约过于宽泛且不具体,它作为边界和接口变得毫无用处。
Consider the repository interface in Listing 21-13. It enables the client to query for domain objects by any means. The contract is flexible and open to extension, but it tells you nothing of the needs or the intent of the retrieving strategies at play in the domain model to obtain collections of aggregates. To truly understand how these methods are used, a developer needs to trawl through all the code to understand the intent and need of queries, let alone how to optimize them. Because the contract is so wide and unspecific, it becomes useless as a boundary and as an interface.
看一下清单 21-14 中不同风格的存储库契约。该契约是明确的,并告诉您很多有关如何使用聚合的信息。开放扩展的方法已经不复存在,取而代之的是两个显式的查询方法,它们的命名方式可以揭示意图并与领域专家的语言保持一致。您正在应用“告诉,不要询问”原则,通过抽象查询方法让存储库完成所有艰苦的工作。现在很清楚如何在基础结构实现中优化这些查询。
Take a look at a different style of repository contract in Listing 21-14. The contract is explicit and tells you a lot about how aggregates are used. Gone are the open-to-extension methods, replaced by two explicit querying methods named in a manner that reveals intent and that is aligned to the language of the domain experts. You are applying the Tell, Don’t Ask principle, making the repository do all the hard work by abstracting the query method. It is now clear how you can optimize these queries in the infrastructure implementation.
存储库是领域模型和持久化存储之间的契约。它应该仅从领域的角度编写,无需考虑底层持久化框架。定义意图并使其明确;不要将存储库契约视为面向对象 (OO) 代码。
The repository is the contract between the domain model and the persistence store. It should be written only in terms of the domain and without a thought to the underlying persistence framework. Define intent and make it explicit; do not treat the repository contract like object-oriented (OO) code.
事务管理主要由应用服务层负责。然而,由于存储库和事务管理紧密相关,因此有必要在此提及它们。(下一章将更详细地介绍事务管理。)存储库仅关注单个聚合根集合的管理,而业务案例可能会导致对多种类型的聚合进行更新。
Transaction management is chiefly the concern of the application service layer. However, because the repository and transaction management are tightly aligned, it’s pertinent to mention them here. (A more detailed examination of transaction management can be found in the following chapter.) A repository is only concerned with the management of a single collection of aggregate roots, whereas a business case could result in updates to multiple types of aggregates.
事务管理由工作单元处理。工作单元模式的作用是跟踪业务任务期间对聚合的所有更改。所有更改完成后,工作单元将协调事务内持久化存储的更新。为了确保在向数据存储区提交更改的过程中出现问题时数据完整性不会受到影响,所有更改都会回滚,以确保数据保持有效状态。
Transaction management is handled by a unit of work. The role of the unit-of-work pattern is to keep track of all changes to aggregates during a business task. Once all changes have taken place, the unit of work then coordinates the updating of the persistence store within a transaction. To ensure that data integrity is not compromised if an issue arises partway through committing changes to the datastore, all changes are rolled back to ensure that the data remains in a valid state.
在 NHibernate 中,工作单元模式由会话对象实现。在清单 21-15 中,一个应用服务依赖于两个存储库来协调积分申请任务。为了管理事务,需要一个以 NHibernate 会话对象形式呈现的工作单元。在由工作单元启动的事务提交之前,任何更改都不会持久化到数据库中。
In NHibernate, the unit of work pattern is implemented by the session object. In Listing 21-15, an application service is dependent on two repositories to coordinate the task of applying loyalty points. To manage the transaction, a unit of work in the form of NHibernate’s session object is required. No changes are persisted to the database until the transaction started by the unit of work is committed.
您无需在应用服务级别抽象底层持久层框架,因为它没有任何价值。在这里,您直接使用 NHibernate 框架,并且是明确的。只有在领域模型中,您才需要保持持久层无关性。在应用服务层,您无需抽象框架,因为它同时协调了基础架构和领域逻辑。
You don’t need to abstract the underlying persistence framework at the application service level because there is no value to it. Here you are being explicit and using the NHibernate framework directly. It is only at the domain model that you need to keep persistence ignorance. At the application service layer, you don’t abstract the framework because it orchestrates both infrastructure and domain logic.
为了使事情更清楚一些,图 21.6展示了存储库如何与会话对象交互(NHibernate 的工作单元模式的实现)。
To make things a little clearer, Figure 21.6 shows how the repositories interact with the session object (NHibernate’s implementation of the unit-of-work pattern).
图 21.6工作单元模式。
FIGURE 21.6 The unit-of-work pattern.
为了确保更改在同一事务中进行,存储库必须使用应用服务层正在使用的会话实例。这是通过使用工厂或控制反转容器来创建应用服务来实现的,这将在下一章详细讨论。存储库实例通过其构造函数注入会话,如清单21-16所示。
To ensure changes are made within the same transaction, the repositories must use the session instance, which the application service layer is using. This is achieved via employment of a factory or inversion of control container to create the application service, which is discussed in detail in the next chapter. The repository instance gets the session injected via its constructor, as shown in Listing 21-16.
或者,您可以使用基于 setter 的注入来确保存储库能够访问与应用服务相同的会话实例。这使得发生的事情更加明确。清单 21-17 展示了如何更新应用服务以利用存储库上基于 setter 的注入。
Alternatively, you can use setter-based injection to ensure the repositories have access to the same session instance as the application service. This makes it even more explicit as to what is going on. Listing 21-17 shows how the application service would be updated to leverage setter-based injection on the repository.
因为工作单元模式与领域无关,纯粹是技术问题,所以将EnlistIn方法直接包含在存储库接口契约中是没有意义的。您可以改为使用一个单独的接口来隐藏 enlist 方法,并确保在执行任何工作之前检查存储库中是否存在会话(参见清单 21-18)。
Because the unit-of-work pattern has nothing to do with the domain and is purely a technical concern, it doesn’t make sense to include the EnlistIn method directly on the repository interface contract. You can instead use a separate interface for this to hide the enlist method and ensure you check for the existence of a session in the repository before carrying out any work (see Listing 21-18).
存储库应该代表聚合的集合,其行为类似于 .NET 集合,完全隐藏底层持久化机制。.NET 集合保存对对象的引用,因此对对象的任何更改都不需要在该集合中显式更新。这意味着您不会在集合中找到保存或更新方法;理想情况下,您的存储库应该以相同的方式运行。然而,模仿类似集合的外观很大程度上取决于您选择的持久化框架的功能。不支持更改跟踪的持久化框架需要公开保存方法,并且协调域活动的应用服务需要确保在对检索到的聚合进行更改后显式持久化它们。如果您选择不利用某种持久化框架,则需要做很多工作。最好了解一些可以帮助您减轻手动持久化繁琐工作的选项。
A repository should represent a collection of aggregates and behave just like a .NET collection, completely hiding the underlying persistence mechanism. A .NET collection holds references to objects, so any changes made to an object doesn’t require them to be explicitly updated within the collection. This means that you won’t find a save or update method on a collection; ideally, your repository would behave in the same manner. However, mimicking a collection-like façade is largely down to the capabilities of your persistence framework of choice. A persistence framework that does not support change tracking needs to expose a save method, and the application service that coordinates domain activity needs to ensure that it explicitly persists retrieved aggregates after changes are made to them. If you are choosing not to leverage some kind of persistence framework, you need to do a lot of work. It’s best to take a look at some of the options that can help you take the chore out of manual persistence.
诸如 NHibernate 之类的 ORM 框架可以跟踪它们检索的对象中的更改。由于更改是隐式跟踪的,因此存储库的客户端(通常是应用服务)不需要显式保存这些对象。这意味着存储库契约中不需要 Save 方法,因为当工作单元标记为完成时,所做的更改就会被推送到持久性存储。考虑清单 21-19,其中显示了存储库契约和示例应用服务。它使用了工作单元模式的抽象,可以由持久性框架实现,也可以手动实现。
ORM frameworks such as NHibernate can track changes in objects that are retrieved by them. Because changes are tracked implicitly, the client of the repository, which is generally the application service, doesn’t need to explicitly save these objects. This means there is no requirement for a Save method on the repository contract because changes made are pushed to the persistence store when the unit of work is marked as complete. Consider Listing 21-19, showing both the repository contract and a sample application service. It uses an abstraction on the unit-of-work pattern, which can be implemented by a persistence framework or manually.
为了跟踪领域对象的变更,持久化框架要么在读取时获取检索到的聚合的快照,并在持久化工作单元时进行比较以确定需要持久化的内容,要么返回聚合的代理并跟踪发生的变更。一旦调用了工作单元的提交,框架就会确定是否发生了任何变更,然后生成在数据模型中持久化该变更所需的 SQL。
To track changes to domain objects, a persistence framework either takes a snapshot of the retrieved aggregates at read time and compares when persisting the unit of work to determine what to persist, or it returns a proxy of the aggregate and tracks changes as they happen. Once the call to the commit on the unit of work is made, the framework determines if any change has occurred and then generates the SQL required to persist that change in the data model.
如果您使用框架来映射不支持更改跟踪的数据模型(例如微型 ORM),或者您正在使用原始 ADO.NET,则需要在存储库契约上支持保存方法,并确保应用程序服务使用更改后的域对象进行调用。清单 21-20 显示了带有 Save 方法的存储库接口以及应用程序服务如何使用它。您仍然可以调用工作单元来确保所有更改都发生在事务中。
If you are using a framework to map a data model that does not support change tracking, such as a micro ORM, or you are using raw ADO.NET, you need to support a save method on the repository contract and ensure that the application service calls with the changed domain object. Listing 21-20 shows a repository interface with the Save method and how the application service would use it. You can still call the unit of work to ensure that all changes occur within a transaction.
当您在具有现有持久性存储的棕地环境中工作时,存储库可以充当防腐层来帮助保持域模型的纯粹性,使您能够创建一个不受任何底层基础设施复杂性影响的模型。
When you’re working in a brownfield environment with an existing persistence store, a repository can help keep the domain model pure by acting as an anticorruption layer, enabling you to create a model without its shape being affected by any underlying infrastructure complexities.
正如您所读到的,数据模型和领域模型可能截然不同。一个数据模型可能分布在多个表甚至数据库中。此外,可以使用多种形式的持久性存储,例如平面文件、Web 服务以及关系型或 NoSQL 存储。无论您使用哪种持久性存储,它都不应影响领域模型。存储库映射到聚合,而不是表;例如,多个实体可以占用一个表,正如第 16 章所述,其中展示了如何对不同状态使用显式实现。存储库还可以将模型存储在多个数据存储中,如图21.7所示。
As you have read, a data model and a domain model can be very different. A data model may be spread over several tables or even databases. Also, multiple forms of persistence store can be used, such as flat files, web services, and relational or NoSQL stores. Whatever persistence store you find yourself using, it should not shape the domain model. Repositories map to aggregates, not tables; for example, more than one entity can occupy a single table, as was covered in Chapter 16, where it was shown how to use explicit implementations for different states. Repositories can also store a model over more than a single data store, as shown in Figure 21.7.
图 21.7存储库充当防腐层。
FIGURE 21.7 The repository acts as an anticorruption layer.
除了聚合的持久化和检索之外,存储库模式还可以公开其领域对象集合的其他事实。例如,存储库可以公开其集合中的聚合数量,如清单 21-21 所示。
Besides the persistence and retrieval of aggregates, the repository pattern can expose other facts about its collection of domain objects. For instance, a repository can expose the number of aggregates in its collection, as shown in Listing 21-21.
如果您的数据库或其他基础设施服务控制 ID 的播种,则可以将其抽象到存储库后面,并将其暴露给应用服务。它可以通过接口上的方法显式提供身份,如清单 21-22 所示。
If your database or another infrastructural service controls the seeding of IDs, you can abstract this behind the repository and expose it to the application service. It can provide identity explicitly via a method on the interface, as shown in listing 21-22.
存储库还可以在持久化期间分配 ID。通常,这是通过将数据存储区作为实体 ID 的种子来实现的。清单 21-23 显示了 NHibernate 的 XML 映射文档的一部分。嵌套的 generator 标签类属性定义了生成实体 ID 的内容。在本例中,它是本机的,底层数据库通过数据库身份种子数据类型自动为实体生成 ID。稍后您将看到用于生成实体标识符的其他选项。NHibernate 使用未保存的值属性与业务实体的标识符进行比较,以帮助确定对象是已持久化还是瞬态(未保存)。
A repository can also assign an ID during persistence. Usually this is accomplished by having the datastore as the seed for an entity’s ID. Listing 21-23 shows part of the XML mapping document for NHibernate. The nested generator tag class attribute defines what generates the ID of the entity. In this case, it is native, and the underlying database automatically generates IDs for the entity via the database identity-seed data type. Later you will see other options for generating the identifier of the entity. The unsaved value attribute is used by NHibernate to compare with the business entity’s identifier to help determine if an object has been persisted or is transient (unsaved).
如果您不熟悉 NHibernate,请不要担心,因为您将在本章末尾完成一些练习,向您展示如何在实践中实现这些模式。
Don’t worry if you are not familiar with NHibernate because you will be working through some exercises at the end of this chapter to show you how to implement these patterns in practice.
除了计算集合中聚合的数量之外,您可能还需要一些关于集合包含内容的其他摘要信息,而不必拉回每个聚合并手动汇总。在清单21-24中,摘要值对象可以对特定类型的客户进行计数。对于最好更接近原始数据运行的密集查询和计算,摘要视图可能非常强大。
Besides counting the number of aggregates you have in a collection, you may want some other summary information on what the collection contains without having to pull back each aggregate and summarize manually. In Listing 21-24, the summary value object can give counts on specific types of customers. For intensive queries and calculations that are best run closer to the raw data, a summary view can be extremely powerful.
当多个用户同时更改域对象的状态时,重要的是用户要针对聚合的最新版本进行操作,并且他们的更改不会覆盖他们不知道的其他更改。并发控制有两种形式:乐观和悲观。乐观并发选项假定多个用户同时更改业务对象的状态不会出现问题,也称为最后更改获胜。对于某些系统来说,这是完全合理的行为;但是,当您的业务对象的状态需要与从数据库检索时的状态一致时,就需要悲观并发。
When multiple users are concurrently changing the state of a domain object, it is important that users are working against the latest version of an aggregate and that their changes don’t overwrite other changes that they are not aware of. There are two forms of concurrency control: optimistic and pessimistic. The optimistic concurrency option assumes that there are no issues with multiple users making changes simultaneously to the state of business objects, also known as last change wins. For some systems, this is perfectly reasonable behavior; however, when the state of your business objects needs to be consistent with the state when retrieved from the database, pessimistic concurrency is required.
悲观并发有多种形式,从检索记录时锁定数据表,到保留业务对象原始内容的副本,并在进行更新之前将其与数据存储区中的版本进行比较,以确保事务期间记录未发生任何更改。许多持久性框架使用版本号来检查业务实体自从数据库检索以来是否已被修改。更新时,在提交更改之前,将业务实体的版本号与数据库中的版本号进行比较。这可确保业务实体自检索以来未被修改。
Pessimistic concurrency can come in many flavors, from locking the data table when a record is retrieved to keeping a copy of the original contents of a business object and comparing that to the version in the datastore before an update is made to ensure there have been no changes to a record during a transaction. Many persistence frameworks use a version number to check whether a business entity has been amended since being retrieved from the database. Upon an update, the version number of the business entity is compared to the version number residing in the database before committing a change. This ensures that the business entity has not been modified since being retrieved.
例如,NHibernate 通过多种方式支持乐观并发。它采用的一种方法是在 XML 映射文件中使用版本标记,如清单 21-25 所示。
As an example, NHibernate supports optimistic concurrency in several ways. One method that it employs is to use a version tag in the XML mapping file, as shown in Listing 21-25.
从数据存储区中检索Product实体时,将设置 Version 属性。如果您对Product实体上的Version属性感到不舒服,因为在您正在建模的领域中,版本不是产品的属性,则可以使用实体层超类型类,如清单 21-26 所示(另请参见第 16 章),或者返回Product实体的代理版本并在其中包含版本 ID。
The Version property is set when the Product entity is retrieved from the datastore. If you feel uncomfortable with the Version property being on the Product entity because in the domain that you are modeling, a version isn’t an attribute of a product, you could use an Entity Layer Supertype class, as shown in Listing 21-26 (see also Chapter 16) or return a proxy version of the Product entity and include the version ID within.
在清单 21-27 中的应用程序服务类中,您可以看到,当将Product实体保存到数据库时,更改后的实体的版本包含在where子句中。NHibernate 比较这些值并抛出一个StaleObjectStateException,因为Person对象在您进行第二次更改后已被修改。然后,程序应该提醒用户,由于原始检索和更新失败, Product实体已更改或被删除。
In Listing 21-27 from an application service class, you can see that when a Product entity is being saved to the database, the version of the changed entity is included in the where clause. NHibernate compares the values and throws a StaleObjectStateException because the Person object has been modified since you made your second change. The program should then alert the user that the Product entity has changed or been deleted since the original retrieval and the update have failed.
在清单 21-27 中,如果在从持久性存储中检索产品并提交更改之间的一小段时间内修改了产品,则会引发异常。更有可能的是,当用户在桌面/浏览器上查看产品时,产品发生了变化。在这种情况下,产品可以在用户查看数据时更新,然后用户可以在不知情的情况下覆盖更改。为了防止这种情况发生,应该使用视图模型将版本标志发送到 UI,并使用UpdatedProductPriceInformation命令返回相同的版本。清单 21-28 中的更新代码检查用户使用的版本是否与从存储库中检索到的版本相同。如果不是,则引发异常,用户可以请求最新的更新数据并在提交价格更新之前再次查看。
In Listing 21-27, an exception is thrown if the product was modified in the small amount of time between retrieving it from the persistence store to commit the changes on it. What is more likely is that a product changes while the user is viewing it on her desktop/browser. In this situation, the product can be updated while the user is reviewing the data, and the user can then overwrite changes unbeknownst to her. To prevent this from happening, the version flag should be sent to the UI with the view model, and the same version should be sent back with the UpdatedProductPriceInformation command. The updated code in Listing 21-28 checks to see if the version that the user was using is the same as the one retrieved from the repository. If it’s not, an exception is thrown, and the user can request the fresh updated data and review it again before submitting a price update.
如果您的数据模型需要的元数据在您的领域中没有意义,您可以利用存储库来满足需求。对象的更改通常需要标记最后更改日期,但这对领域来说毫无意义。您可以通过存储库实现来应用这些元数据。
If your data model requires metadata that does not make sense in your domain, you can utilize your repository to meet the requirements. Often changes to objects need to be marked with a last change date, which has no meaning to the domain. You can apply this metadata via the repository implementation.
审计跟踪和日志记录也可以通过存储库提供。如果您要删除某个聚合,存储库可以记录该事件以供审计,并且可能还会生成该聚合的快照或摘要。
Audit trails and logging can also be supplied via the repository. If you are deleting an aggregate, the repository can log the event for auditing and perhaps take a snapshot of the aggregate or a summary of it.
与任何模式一样,也有一些建议的做法应该避免。这些做法被称为反模式。
As with any pattern there are a number of recommended practices that should be avoided. These are known as antipatterns.
存储库是领域模型和持久性机制之间的显式契约。通过公开一个用于临时查询的通用 catch-all 方法,您正在削弱这一抽象层。考虑清单 21-29 中的契约。
A repository is an explicit contract between the domain model and the persistence mechanism. By exposing a generic catch-all method for querying in an ad hoc manner, you are weakening this layer of abstraction. Consider the contract in Listing 21-29.
FindBy方法接受某种形式的客户查询,可能以规范的形式出现。这里的挑战在于客户端可以编写任何类型的查询,这意味着所有获取路径都需要优化。这样的方法会削弱存储库的契约,并完全消除查询存储库的意图和意义。存储库不应开放扩展;它本质上是一个边界和过程。相反,应该优先使用命名方法,以便您可以编写可进行性能调优并根据底层持久化框架进行定制的特定代码。然后,可以调整添加的新方法,并创建最合适的获取策略。
The FindBy method takes some form of customer query, perhaps in the form of a specification. The challenge here is that the client can write any type of query, meaning all fetch paths need to be optimized. A method like this weakens the repository’s contract and completely removes the intent and meaning of querying the repository. The repository should not be open to extension; it is a boundary and procedural in nature. Instead, favor named methods so that you can write specific code that can be performance tuned and tailored to the underling persistence framework. New methods that are added can then be tuned, and the most appropriate fetching strategy can be created.
在存储库上公开IQueryable是支持临时查询方法的另一种方式。IQueryable接口是一种极其灵活的数据访问模式;但是,它可以使您的数据模型泄漏到域模型中。在存储库边界后面使用IQueryable接口或直接访问数据模型以满足报告需求并没有错,如清单21-30所示。
Exposing IQueryable on a repository is another flavor of supporting ad hoc query methods. The IQueryable interface is an extremely flexible pattern for data access; however, it can enable your data model to leak into your domain model. There is nothing wrong with using the IQueryable interface behind the boundary of your repository or directly accessing the data model for reporting needs, as shown in Listing 21-30.
聚合应该围绕不变量构建,并包含执行这些不变量所需的所有属性。因此,加载聚合时,要么全部加载,要么全部不加载。如果您使用关系数据库并使用 ORM 作为数据模型,则可以延迟加载某些领域对象属性。这使您能够推迟加载聚合中不需要的部分。然而,这样做的问题在于,如果您只加载了部分聚合,则您的聚合边界可能存在错误。此外,如果数据获取模式影响了您的聚合,则您可能围绕视图而不是业务规则构建了聚合。
Aggregates should be built around invariants and contain all the properties necessary to enforce those invariants. Therefore, when loading an aggregate, you need to load all of it or none of it. If you have a relational database and are using an ORM for your data model, you may be able to lazy load some domain object properties. This enables you to defer loading parts of an aggregate you don’t need. However, the problem with this is that if you can get away with only partially loading an aggregate, you probably have your aggregate boundaries wrong. Also, if data-fetching patterns are affecting your aggregates, you may have built aggregates around views and not business rules.
用户界面屏幕的需求和业务逻辑的需求截然不同。屏幕所需的信息与单个聚合根中包含的数据之间经常存在不匹配的情况。如果只需要一两个属性用于显示,则需要对整个聚合进行数据融合。更糟糕的是,您的领域对象可能需要额外的信息来支持视图,而这些信息对于它们所附加的领域对象来说可能毫无意义。
The needs of your user-interface screens and the needs of business logic are very different. There will often be a mismatch between information required for a screen and the data contained within a single aggregate root. If only one or two properties are required for display purposes, an entire aggregate needs to be hydrated. Worse still, your domain objects may need extra information to support views that may make little sense to the domain objects they are attached to.
此外,如果您正在为跨越多个聚合根的屏幕创建视图模型,则需要将它们全部拉回来并挑选出所需的信息。
Further still, if you’re creating a view model for a screen that spans multiple aggregate roots, you need to pull them all back and pick out the information you require.
使用事务模型运行报告可能会很慢,并且由于 UI 屏幕的附加属性而损害模型的完整性。最好使用框架直接查询读取存储;这可以是用于事务工作的同一数据存储,也可以是非规范化的存储。
Running reports using a transaction model can be slow and compromise the integrity of your model because of the additional properties for UI screens. It is better to use a framework to directly query a read store; this could be the same datastore you use for the transactional work, or it could be a denormalized store.
在查询数据模型以用于报表目的时,请勿抽象持久化框架。您将在下一章中阅读更多相关内容。
Don’t abstract the persistence framework when querying your data model for reporting purposes. You’ll read more about this in the next chapter.
在本节中,您将了解一些基于流行的 .NET 持久性框架的存储库实现,即:
In this section, you will work through some repository implementations based on popular .NET persistence frameworks, namely:
在本例中,您将使用一个基于 eBay 竞价逻辑的小型领域模型。图 21.8展示了您将使用的领域模型。您将不会遵循测试优先的开发方法,因为这是一个练习,旨在向您展示存储库的实现,而不是如何驱动领域模型的设计。考虑到这一点,您将以一种您可以跟随并自行创建的方式构建解决方案。
For the examples, you will use a small domain model based on the bidding logic of eBay. Figure 21.8 shows the domain model you will be using. You will not be following a test-first development methodology, because this is an exercise to show you an implementation of a repository rather than how to drive the design of your domain model. With this in mind, you will build up the solutions in a manner where you can follow along and create them yourself.
图 21.8解决方案对象模型。
FIGURE 21.8 Solution object model.
该模型代表处理 eBay 自动竞价机制的逻辑:
The model represents the logic that handles eBay’s automatic bidding mechanism:
您将使用的每个持久化框架都会对您的领域模型产生微小的影响。这些示例将证明,无论您使用哪种持久化框架,您仍然可以支持持久化无关的领域模型。
Each of the persistence frameworks that you will use will have a small impact on your domain model. The examples will demonstrate that no matter what persistence framework you are using, you can still support a domain model that is persistent ignorant.
前两个存储库实现示例分别使用了 NHibernate 和 RavenDB。这两个框架都允许将领域模型直接映射到数据模型,且几乎不做任何妥协。
The first two examples of repository implementation use NHibernate and RavenDB. Both of these frameworks allow direct mapping of a domain model to a data model with little or no compromise.
NHibernate 是流行的 Java 开源框架 Hibernate 的一个端口。这是一个已经存在多年的框架,是一个久经考验的强大软件。NHibernate 是一个 ORM。NHibernate 的最佳特性之一是支持持久性忽略;这意味着您的业务对象不必从基类继承或实现框架接口。NHibernate 使用ISession的实例,它是工作单元模式的一种实现。ISession也是您用来查询数据库的对象。它充当持久性管理器和数据库的网关,允许您对其进行查询,以及保存、删除和添加实体。在NHibernate中有多种方法可以将业务对象映射到数据库表。最流行的方法之一是通过 XML 配置文件,但也可以使用属性和流畅的代码映射选项。此示例使用较旧的 XML 映射样式。
NHibernate is a port of the popular open source Hibernate framework for Java. It’s a framework that has been around for years, and it’s a proven and robust piece of software. NHibernate is an ORM. One of the best features of NHibernate is the support for persistence ignorance; this means that your business objects don’t have to inherit from base classes or implement framework interfaces. NHibernate uses an instance of an ISession, which is an implementation of the unit of work pattern. ISession is also the object you use to query your database with. It acts as your persistence manager and gateway into the database, allowing you to query against it, as well as saving, deleting, and adding entities. There are a number of ways to map business objects to database tables in NHibernate. One of the most popular is via an XML configuration file, but attributes and a fluent code mapping option are also available. This example uses the older XML mapping style.
本例中的领域模型没有公共属性,并且是完全封装的。您可以使用 NHibernate 的功能来持久化封装的模型。
The domain model in this example has no public properties and is totally encapsulated. You use NHibernate’s ability to persist encapsulated models.
创建一个名为DDDPPP.Chap21.NHibernateExample的新空白 Visual Studio 解决方案,并向其中添加一个名为DDDPPP.Chap21.NHibernateExample.Application的类库和一个名为DDDPPP.Chap21.NHibernateExample.Presentation的控制台应用程序。在Presentation项目中添加对该应用程序的引用。接下来,在DDDPPP.Chap21.NHibernateExample.Application项目中创建以下文件夹:
Create a new blank Visual Studio solution named DDDPPP.Chap21.NHibernateExample and add to this a class library named DDDPPP.Chap21.NHibernateExample.Application and a console application named DDDPPP.Chap21.NHibernateExample.Presentation. Within the Presentation project, add a reference to the application. Next, create the following folders in the DDDPPP.Chap21.NHibernateExample.Application project:
您可以删除自动创建的Class1.cs,因为您不需要它。您的解决方案现在应该与图 21.9一致。
You can delete Class1.cs that is automatically created because you won’t be needing it. Your solution should now match Figure 21.9.
图 21.9 Visual Solution 项目结构
FIGURE 21.9 Visual Solution project structure
在构建应用程序之前,您需要引用 NHibernate 程序集。打开 NuGet 包管理器并安装 NHibernate,如图21.10所示。
Before you build out the application, you need to reference the NHibernate assemblies. Open the NuGet package manager and install NHibernate, as shown in Figure 21.10.
图 21.10 NuGet 包管理器
FIGURE 21.10 NuGet Package Manager
您要做的第一件事是创建模型。您将使用领域事件,因此您需要创建基础结构来支持引发和处理事件。您将使用在第 18 章“领域事件”中构建的框架。在应用程序类库项目的Infrastructure文件夹中添加一个名为DomainEvents 的新类,如清单 21-31 所示。您可以在第 18 章中找到有关此类如何工作的详细信息。
The first thing you are going to do is to create the model. You will be utilizing domain events, so you need to create the infrastructure to support raising and handling events. You will use the framework that you built in Chapter 18, “Domain Events.” Add a new class to the Infrastructure folder of the Application class library project called DomainEvents with Listing 21-31. You can find details about how this class works in Chapter 18.
您还将使用第 15 章“值对象”中介绍的值对象基类。在Infrastructure文件夹中创建ValueObject类,如清单 21-32 所定义。
You will also be using the value object base class that was covered in Chapter 15, “Value Objects.” Create the ValueObject class, as defined in Listing 21-32, in the Infrastructure folder.
最后一部分基础结构是为模型中的实体创建一个基类,如清单 21-33 所示。
The last piece of infrastructure is to create a base class for the entities within the model, as shown in Listing 21-33.
整理好基础架构后,就可以构建领域模型了。在DDDPPP.Chap21.NHibernateExample.Application项目的Model文件夹中,创建以下两个文件夹,它们将包含构成领域模型的两个聚合。
With the infrastructure sorted, you can build out the domain model. From within the Model folder of the DDDPPP.Chap21.NHibernateExample.Application project, create the following two folders that will contain the two aggregates that make up the domain model.
您将构建的第一个聚合是Auction聚合。Auction聚合包含有关投标的所有域规则。您需要一个值对象来表示模型中的金钱概念。使用清单 21-34,在Auction文件夹中创建一个名为Money的值对象。
The first aggregate that you will build is the Auction aggregate. The Auction aggregate contains all the domain rules around placing bids. You need a value object to represent the money concept in the model. Using Listing 21-34, create a value object named Money in the Auction folder.
正如你在第十五章中读到的,值对象是不可变的;你无法改变它的状态。这就是为什么add方法返回一个新的Money对象,而不是改变对象本身的状态。
As you read in Chapter 15, a value object is immutable; you cannot alter its state. This is why the add method returns a new Money object rather than changing the state of the object itself.
在构造函数中,有一小段逻辑可以确保金额非负。如果不是,则抛出清单 21-35 中的两个异常之一。将这两个类添加到Auctions文件夹中。
In the constructor, there is a small piece of logic that ensures you have a nonnegative money amount. If not, one of the following two exceptions in Listing 21-35 is thrown. Add these two classes to the Auctions folder.
拍卖域中的下一个概念是出价。出价是指竞标者愿意为拍卖中的一件物品支付的价格。之所以有出价的概念,是因为只有超过出价增量的出价才会转化为出价。Offer类也是一个值对象。使用清单 21-36 所示的代码在Auction文件夹中创建Offer类。
The next concept that you have in your auction domain is that of an offer. An offer is how much a bidder is willing to pay for an item in the auction. The reason you have the concept of an offer is that only offers that exceed the bid increment are turned into bids. The Offer class is also a value object. Create the Offer class within the Auction folder with the code shown in Listing 21-36.
拍卖价格代表当前中标价格;该价格有一个计算出价增量的方法。这又是一个值对象。将Price类添加到Model文件夹中,如清单 21-37 所示。在真正的 eBay 竞价系统中,出价增量级别要多得多。为了简化练习,这里只列出三个。
The auction price represents the current price of the winning bid; the price has a method that calculates the bid increment. This is again a value object. Add the Price class to the Model folder with Listing 21-37. In the real eBay bidding system, there are far more bid increment levels. Here there are only three to keep the exercise simple.
WinningBid是另一个值对象,表示投标人接受的报价。在Model文件夹中创建WinningBid类,如清单 21-38 所示。
The WinningBid, yet another value object, represents a bidder’s accepted offer. Create the WinningBid class within the Model folder with Listing 21-38.
领域服务AutomaticBidder代表会员出价,如果该会员的出价高于其最高出价,则出价最高可达其最高出价。它只出价到保持最高出价者所需的金额,最高可达出价者的最高金额。如清单 21-39 所示,在Model文件夹中创建AutomaticBidder类。
The domain service AutomaticBidder bids on a member’s behalf up to his maximum bid if he is outbid. It bids only as much as needed to remain the highest bidder, up to the bidder’s maximum amount. Create the AutomaticBidder class in the Model folder with Listing 21-39.
BidPlaced是一个领域事件,表示已发出投标。如清单 21-40 所示,将该类添加到Model文件夹中。
BidPlaced is a domain event signifying that a bid was placed. Add the class to the Model folder using Listing 21-40.
OutBid类是另一个领域事件。当投标人出价高于对方时,就会引发该事件,如清单 21-41 所示。
The OutBid class is another domain event. It is raised if a bidder is outbid, as shown in Listing 21-41.
清单 21-42 所示的Auction是一个实体和聚合根。Auction类没有公共属性,只公开了一个方法来进行投标。投标结果将作为领域事件引发。
The Auction shown in Listing 21-42 is an entity and the aggregate root. The Auction class has no public properties and only exposes a single method to enable bids to be placed. The results of bids placed are raised as domain events.
请注意,AutomaticBidder类是直接在Auction类中实例化的,而不是作为依赖项注入。当给定依赖项接口有多个实现可供选择时,依赖注入模式非常有用,但由于 AutomaticBidder 只有一个实现,并且无需模拟或存根领域服务,因此您可以安全地直接实例化它。
Notice that the AutomaticBidder class is instantiated directly within the Auction class as opposed to being injected as a dependency. The dependency injection pattern is useful when there is a selection of multiple implementations of a given dependency interface, but because there’s only one implementation of the AutomaticBidder and there is no need to mock or stub the domain service, you can safely instantiate it directly.
Auction聚合的存储库契约只有两种方法:一种是通过 ID 查找拍卖,另一种是添加拍卖。NHibernate 可以跟踪更改,因此存储库可以像集合接口一样工作;无需向存储库添加显式的保存方法。将清单 21-43 中的契约定义添加到Auction文件夹中。这是在infrastructure文件夹中实现的。
The repository contract for the Auction aggregate has only two methods: one to find an auction by ID and one to add an auction. NHibernate can track changes, so the repositories can act like collection interfaces; there is no need to add an explicit save method to the repository. Add the contract definition in Listing 21-43 to the Auction folder. This is implemented within the infrastructure folder.
第二个也是最后一个聚合实际上只是一个代表拍卖竞价历史的值对象的集合。使用清单 21-44,在BidHistory文件夹中创建一个名为Bid的新值对象。
The second and final aggregate is really only a collection of value objects that represent the bidding history of an auction. Using Listing 21-44, create a new value object named Bid within the BidHistory folder.
此聚合的唯一其他项目是存储库合约,如清单 21-45 所示。存储库公开了一个方法来返回拍卖的出价数量,并有一个方法来持久化新的出价。
The only other item for this aggregate is the repository contract, as shown in listing 21-45. The repository exposes a method to return the number of bids placed against an auction and has a method to persist new bids.
应用程序的领域模型现已完成。您的解决方案应如图 21.11所示。
The domain model of the application is now complete. Your solution should look like Figure 21.11.
图 21.11 Visual Studio 解决方案结构
FIGURE 21.11 The Visual Studio solution structure
模型完成后,现在将构建应用服务层,它是领域模型的客户端。在Application文件夹中,添加两个文件夹:
With the model complete, you will now build the application service layer, which is the client of the domain model. Within the Application folder, add two folders:
BusinessUseCases文件夹包含应用程序的所有功能和用例,而Queries文件夹包含报告域模型状态所需的所有查询。
The BusinessUseCases folder contains all the features and use cases of the application, whereas the Queries folder contains all the queries required to report on the state of the domain model.
创建拍卖需要起始价格和结束日期。应用服务的客户端使用数据传输对象(DTO)来携带创建拍卖的信息。使用清单 21-46,在BusinessUseCases文件夹中创建 DTO。
Creating an auction requires a starting price and an end date. Clients of the application service use a data transfer object (DTO) to carry the information to create an auction. Using Listing 21-46, create the DTO within the BusinessUseCases folder.
处理请求的应用服务详见清单 21-47。CreateAuction应用服务只是创建一个新的拍卖并将其添加到存储库中。唯一值得注意的是,您将操作包装在 NHibernate 事务中。应用服务能够通过构造函数获取 NHibernate 的工作单元模式,即ISession变量。正如您稍后将看到的,通过构造函数传递的ISession与存储库实现中使用的相同,这是工作单元和应用服务中的存储库之间的关系。这确保了应用服务层控制何时将更改提交给域对象。
The application service that handles the request is detailed in Listing 21-47. The CreateAuction application service simply creates a new auction and adds it to the repository. The only thing of note is that you are wrapping the action within an NHibernate transaction. The application service is able to obtain NHibernate’s unit of work pattern, the ISession variable, via the constructor. As you will see later, the ISession that is passed through the constructor is the same as what’s used in the repository implementation this is the relationship between the unit of work and the repository within the application service. This ensures that the application service layer controls when changes are committed to domain objects.
ISession是持久化和检索业务实体的主要接口,可以被认为是 NHibernate 通往数据库的网关。NHibernate 站点将ISession定义为“持久化管理器”。ISession是NHibernate的工作单元实现。由于ISession接口实现了本章前面讨论过的工作单元模式,因此在事务提交之前不会发生任何更改。NHibernate 内置的另一个模式是身份映射,无论检索多少次,它都会在ISession中维护一个业务实体的单一实例。
The ISession is the main interface that persists and retrieves business entities and can be thought of as NHibernate’s gateway to the database. The NHibernate site defines ISession as the “persistence manager.” The ISession is NHibernate’s unit of work implementation. Because the ISession interface implements the unit of work pattern discussed earlier in this chapter, no changes occur until a transaction is committed. Another pattern built into NHibernate is identity map, which maintains a single instance of a business entity in the ISession no matter how many times you retrieve it.
时间是拍卖领域中的一个重要概念,只有在拍卖仍然有效时才能出价。与往常一样,您永远不想将域对象绑定到系统时钟,因为这会使测试变得困难。因此,考虑到这一点,您将通过在Infrastructure文件夹中定义一个名为IClock的接口来抽象时钟的概念,如清单 21-48 所示。
Time is an important concept in the auction domain, and bids should only be able to be placed while the auction is still active. As always, you never want to tie your domain objects to the system clock because it makes testing difficult. So with this in mind, you will abstract the concept of a clock by defining an interface named IClock within the Infrastructure folder, as shown in Listing 21-48.
对于应用程序,您将使用系统时钟。同样在Infrastructure文件夹中创建IClock接口的实现,如清单 21-49 所示。
For the application, you will use the system clock. Create an implementation of the IClock interface, again within the Infrastructure folder, using Listing 21-49.
您将为其创建应用服务的第二个业务用例是拍卖竞标任务。使用清单21-50,创建一个名为BidOnAuction 的类。
The second business use case that you will create an application service for is the task of bidding on an auction. Using Listing 21-50, create a class named BidOnAuction.
再次,您将方法调用包装在 NHibernate 事务中。这一点很重要,因为成功的出价会引发一个领域事件,从而导致出价被添加到BidHistoryRepository中。出价调用也包装在一个带有StaleObjectStateException的 try catch 中,导致再次出价。如果在从存储库检索拍卖和提交工作单元之间,其他成员更新了拍卖,则会抛出StaleObjectStateException。在本练习中,您将尝试使用刷新后的拍卖再次出价,以便与最新版本的拍卖进行交互。
Again, you wrap the method call in an NHibernate transaction. This is important because a successful bid raises a domain event, which results in a bid being added to the BidHistoryRepository. The call to place a bid is also wrapped in a try catch with the StaleObjectStateException, resulting in the bid being placed again. The StaleObjectStateException is thrown if the auction is updated by another member between it being retrieved from the repository and the unit of work committing. In this exercise, you try again to place the bid with the refreshed auction to work against the latest version.
模型和应用服务层完成后,您可以专注于 NHibernate 存储库实现。您需要做的第一件事是映射聚合。使用清单 21-51,将一个名为Auction.hbm.xml的新 XML 文件添加到Infrastructure文件夹中。
With the model and application service layers complete, you can focus on the NHibernate repository implementation. The first thing you need to do is map the aggregates. Using Listing 21-51, add a new XML file to the Infrastructure folder named Auction.hbm.xml.
本章不会详细介绍这些文件的语法,因为这不是一本关于如何使用 NHibernate 的书,但理解 NHibernate 如何将列和表映射到业务实体和属性应该很容易。想要深入了解 NHibernate 的世界,可以查阅大量的在线资源或阅读《NHibernate 实战》一书。
This chapter doesn’t go into detail about the syntax of these files because this is not a book on using NHibernate, but it should be easy to work out how NHibernate maps columns and tables to business entities and properties. For a deeper insight into the world of NHibernate, check out the many online resources or the book NHibernate in Action.
为了让 NHibernate 选择映射文件,请确保将字段的 Build Action 属性更改为 Embedded Resource,如图21.12所示。
For NHibernate to pick up the mapping file, ensure that you change the Build Action property of the field to Embedded Resource, as shown in Figure 21.12.
图 21.12 XML 映射文件的构建操作属性
FIGURE 21.12 The build action property of the XML Mapping file
要映射投标,请将第二个 XML 文件添加到名为Bid.hbm.xml的基础设施文件夹中,如清单 21-52 所示,再次为文件的 Build Action 属性选择 Embedded Resource。
To map a bid, add a second XML file to the Infrastructure folder named Bid.hbm.xml with Listing 21-52, again selecting Embedded Resource for the Build Action property of the file.
存储库实现位于基础设施命名空间下。第一个是拍卖存储库实现,如清单 21-53 所示。
The repository implementations live under the infrastructure namespace. The first is the auction repository implementation, as defined in Listing 21-53.
如你所见,ISession变量完成了所有工作,因此,通过构造函数注入的实例必须与注入到应用服务构造函数中的实例相同。否则,应用服务将无法控制工作单元。
As you can see, the ISession variable does all the work, which is why it’s vital that the instance that is injected via the constructor is the same that is injected into the constructors of the application services. Otherwise, the application service can’t control the unit of work.
BidHistory存储库的实现(如清单 21-54 所示)同样简单,唯一的区别是增加了一个简单的查询来提供有关拍卖出价数量的摘要信息。
The implementation for the BidHistory repository, shown in listing 21-54, is equally simple, with the only difference being the addition of a simple query to provide summary information on the number of bids for an auction.
使用 NHibernate 有一些限制。其一是所有要持久化的对象都需要一个无参数的构造函数。幸运的是,构造函数可以是私有的,所以这不会对你的模型产生影响。对于一个允许你的领域模型为 POCO 的持久化框架来说,这只是一个小小的代价。
There are a few constraints to using NHibernate. One is that all objects being persisted need to have a parameterless constructor. Luckily, the constructor can be private, so this will have no effect on your model. This is a small price to pay for a persistence framework that allows your domain model to be POCO.
更新Auction类并添加一个私有构造函数,如清单 21-55 所示。
Update the Auction class and add a private constructor, as shown in Listing 21-55.
更新WinningBid类并添加一个私有构造函数,如清单 21-56 所示。
Update the WinningBid class and add a private constructor, as shown in Listing 21-56.
更新Price类并添加一个私有构造函数,如清单 21-57 所示。
Update the Price class and add a private constructor, as shown in Listing 21-57.
您可能已经注意到,在映射中为Bid类映射了一个 ID 属性。NHibernate 需要为其映射的每个对象提供一个 ID。更新Bid类,并添加一个私有构造函数和一个新属性来保存将由 NHibernate 设置的 ID,如清单 21-58 所示。
You may have noticed in the mapping that you mapped an ID property for the Bid class. NHibernate needs to have an ID present for each object that it maps. Update the Bid class and add a private constructor and a new property to hold an ID that will be set by NHibernate, as shown in Listing 21-58.
您可能还注意到,拍卖实体的映射有一个 version 属性。此属性确保数据不会过时(自将其从数据库拉取后未发生任何变化)。NHibernate 在持久化实体时,会检查版本是否相同;如果不一致,则会抛出StaleObjectStateException异常。此异常由应用服务和拍卖域处理;如果发生这种情况,请尝试再次应用该报价。
You may have also noticed that the mapping for the Auction entity had a version property. This property ensures that the data is not stale (it hasn’t changed since pulling it from the database). When NHibernate persists the entity, it checks to ensure that the version is the same; if it’s not, it throws a StaleObjectStateException. This exception is handled by the application service and in your auction domain; if this happens, try to apply the offer again.
向Entity类添加一个版本属性,如清单 21-59 所示。
Add a version property to the Entity class, as shown in Listing 21-59.
对于数据库,您可以使用免费的 MS SQL Express(http://www.microsoft.com/web/platform/database.aspx)。您或许可以从映射中计算出模式;事实上,NHibernate 确实可以根据映射构建数据库。但是,这里是完整的数据库模式。创建一个名为AuctionExample的数据库,并运行清单 21-60 进行设置。
For the database you can use the free MS SQL Express (http://www.microsoft.com/web/platform/database.aspx). You can probably work out the schema from the mapping; in fact, NHibernate can actually build the database from the mapping. However, here is the full database schema. Create a database named AuctionExample and run Listing 21-60 to set it up.
您的表模式将类似于图 21.13。
Your table schema will resemble Figure 21.13.
图 21.13数据库模式
FIGURE 21.13 The database schema
您将把注意力转向应用服务的查询端。查询服务报告拍卖的状态。拍卖的第一个视图是其状态的摘要。您将使用一个简单的视图模型,而不是返回域对象。使用清单 21-61,在Queries文件夹中创建AuctionStatus类。
You will turn your attention to the query side of the application service. The query services report on the state of the auctions. The first view of the auction is a summary of its status. Instead of returning the domain object, you will use a simple view model. Using Listing 21-61, create the AuctionStatus class within the Queries folder.
拍卖状态查询需要使用原生 SQL,因为没有公开的 getter 将聚合数据转换为AuctionStatus DTO。然而,这可以将领域模型与应用程序的报告需求清晰地分离,并避免存储库处理报告相关的问题。
The auction status query requires native SQL to be used because you have no public getters to transform the aggregate into an AuctionStatus DTO. However, this cleanly separates the domain model from the application’s reporting needs and prevents the repository from having to deal with reporting concerns.
应用程序需要访问的另一个报告是拍卖的出价历史记录。同样,您不想公开域对象,因此您将创建一个特定的 DTO,如清单 21-63 所示。尽管它与真实的Bid域对象非常相似,但BidInformation类代表了完全不同的关注点,并且不会受到Bid值对象演变的影响。
The other report that the application needs to give access to is the history of bids against the auction. Again, you don’t want to expose your domain objects, so you will create a specific DTO, shown in Listing 21-63. Even though it closely resembles the real Bid domain object, the BidInformation class represents a completely different concern and will not be affected if the Bid value object evolves.
同样,由于封装的域模型,清单 21-64 中的查询服务需要直接进入数据库来获取有关拍卖出价的信息,从而获取视图。
Again, the query service, Listing 21-64, that pulls back information on the bids placed against an auction needs to go directly to the database to pull back a view because of the encapsulated domain model.
NHibernate 需要一小段配置才能工作。更新Presentation项目的app.config,使其与清单 21-65 一致。然后根据数据库实例名称更改连接字符串。
NHibernate needs a small piece of configuration to work. Update the app.config of the Presentation project so that it matches Listing 21-65. Then change the connection string depending on your database instance name.
为了将所有依赖项绑定在一起,您将使用控制反转容器。所选的容器是 StructureMap。使用 NuGet 包管理器安装 StructureMap,就像安装 NHibernate 包一样,但这次要确保两个项目中都引用了 StructureMap。添加一个名为Bootstrapper的类(清单 21-66),用于连接应用服务层依赖项。
To tie all the dependencies together, you will utilize an inversion-of-control container. The container of choice will be StructureMap. Install StructureMap using the NuGet package manager just as you did with the NHibernate package, but this time ensure that StructureMap is referenced in both projects. Add a class named Bootstrapper, Listing 21-66, that wires the application service layer dependencies.
ISessionFactory通常以单例对象的形式创建,因为创建它的操作相对昂贵。ISessionFactory 的任务之一是提供ISession实例。您可以使用StructureMap来确保在一个流程中,所有存储库和所有应用服务都使用相同版本的ISession变量,这意味着应用服务可以控制工作单元。
The ISessionFactory is typically created as a singleton object because of the relatively expensive operation of creating it. One of the jobs of the ISessionFactory is to provide ISession instances. You use StructureMap to ensure that the same version of the ISession variable is used for all repositories and all application services during a process, meaning that the application service can control the unit of work.
最后,为了展示应用程序的运行情况,您将模拟用户在拍卖会上出价。将清单21-67添加到Presentation项目的程序文件中。
Finally, to show the application in action, you will simulate users bidding on an auction. Add Listing 21-67 to the Program file of the Presentation project.
图 21.14显示了程序的运行情况。
Figure 21.14 shows the program running.
图 21.14正在运行的程序
FIGURE 21.14 The running program
RavenDB 是一个无模式文档数据库,它将领域对象存储为 JSON(JavaScript 对象表示法)文档。因此,数据和领域模型之间不存在不匹配的情况。在本例中,您将更改领域模型,使属性公开,但将 setter 保持为私有。
RavenDB is a schemaless document database that stores domain objects as JSON (JavaScript Object Notation) documents. Therefore, there is no mismatch between the data and the domain model. In this example, you change the domain model so that the properties are public, but you keep the setters private.
在开始使用 Visual Studio 解决方案之前,您需要安装 RavenDB。安装 RavenDB 最简单的方法是从 RavenDB 官网http://ravendb.net/download下载最新的安装程序。每个 RavenDB 服务器实例都可以通过一个可远程访问的 Silverlight 应用程序 RavenDB Management Studio 进行管理。安装完成后,您可以访问http://localhost:8080/查看 RavenDB Management Studio。
Before you start with the Visual Studio solution, you need to install RavenDB. The easiest way to install RavenDB is to download the latest installer from RavenDB’s website at http://ravendb.net/download. Every RavenDB server instance is manageable via a remotely accessible Silverlight application: the RavenDB Management Studio. After installing it, you can navigate to http://localhost:8080/ to view the RavenDB Management Studio.
安装 RavenDB 后,创建一个名为DDDPPP.Chap21.RavenDBExample的新空白 Visual Studio 解决方案,并在其中添加一个名为DDDPPP.Chap21.RavenDBExample.Application的类库和一个名为DDDPPP.Chap21.RavenDBExample.Presentation的控制台应用程序。在Presentation项目中添加对该应用程序的引用。接下来,在DDDPPP.Chap21.RavenDBExample.Application项目中创建以下文件夹:
After installing RavenDB, create a new blank Visual Studio solution named DDDPPP.Chap21.RavenDBExample and add to this a class library named DDDPPP.Chap21.RavenDBExample.Application and a console application named DDDPPP.Chap21.RavenDBExample.Presentation. Within the Presentation project, add a reference to the application. Next, create the following folders in the DDDPPP.Chap21.RavenDBExample.Application project:
您可以删除自动创建的Class1.cs,因为您不需要它。使用 NuGet 安装 RavenDB 客户端库,如图21.15所示。
You can delete the Class1.cs that is automatically created because you won’t be needing it. Use NuGet to install the RavenDB client libraries, as shown in Figure 21.15.
图 21.15通过 NuGet 安装 RavenDB 客户端库。
FIGURE 21.15 Install RavenDB client libraries via NuGet.
按照与 NHibernate 示例相同的方式构建应用程序,或者直接将类文件复制到这个新的解决方案中,并确保更新命名空间。删除以下特定于 NHibernate 示例的类:
Build out the application in the same manner as you did for the NHibernate example, or simply copy the class files into this new solution, ensuring you update the namespaces. Remove the following classes that were specific to the NHibernate example:
您将得到类似于图 21.16 的解决方案。
You are left with a solution that resembles Figure 21.16.
图 21.16解决方案资源管理器中有一个基于 NHibernate 示例的应用程序。
FIGURE 21.16 Solution explorer with an application based on the NHibernate sample.
您删除的类将被 RavenDB 的存储库实现替换。
The classes you removed are replaced with RavenDB’s repository implementation.
该模型与 NHibernate 示例中构建的模型几乎相同。它仍然需要每个领域对象中都有一个无参数的构造函数。由于使用公共 getter 进行建模,因此需要更改Auction类的属性以匹配清单 21-68。
The model is nearly identical to the model that was built in the NHibernate example. It still requires a parameterless constructor in each of the domain objects. As you are modeling with public getters, you need to change the properties of the Auction class to match Listing 21-68.
Auction类中还有一个名为HasBeenBidOn的公共方法,该方法将在应用服务查询方法中用于报告拍卖的状态。唯一的其他更改是将Money类的getter Value属性设为公共,以便可以对其进行报告,如清单 21-69 所示。
There is also a public method on the Auction class named HasBeenBidOn that will be used within the application service query method to report on the state of the auction. The only other change is to make the getter Value property of the Money class public so you can report on it, as shown in Listing 21-69.
RavenDB 示例所需的附加类是包含BidHistory域对象。该对象包含按顺序对拍卖发出的所有出价。将此类添加到域模型的BidHistory文件夹中,如清单 21-70 所示。
An additional class that is required for the RavenDB example is the inclusion of the BidHistory domain object. This object contains all the bids placed against an auction in the order they were placed. Add this class to the BidHistory folder of the domain model with Listing 21-70.
要检索BidHistory对象,需要向IBidHistoryRepository合约添加一个新方法,如清单 21-71 所示。
To retrieve the BidHistory object, you need to add a new method to the IBidHistoryRepository contract, as shown in Listing 21-71.
该应用服务与 NHibernate 解决方案的唯一不同之处在于它使用 RavenDB 的IDocumentSession类型,而不是 NHibernate 的ISession类型。如果并发中断,还会抛出一个不同的异常。RavenDB 支持隐式事务,这意味着它们内置于IDocumentSession中。您不需要显式地将调用包装在事务中,如清单 21-72 所示。
The application service only differs from the NHibernate solution because it uses RavenDB’s IDocumentSession type as opposed to NHibernate’s ISession. There is also a different exception that is thrown if concurrency is broken. RavenDB supports implicit transactions, which means they are built in to the IDocumentSession. You don't need to explicitly wrap the call in a transaction, as shown in Listing 21-72.
在 RavenDB 中,您无需使用Entity基类的Version属性,因为它默认内置了版本标记。从 RavenDB 加载文档时,它会缓存与其相关的 Etag。Etag 本质上就是版本标记。当调用提交会话时,RavenDB 会检查 Etag 自检索文档以来是否已更新。如果已更新,则会抛出ConcurrencyException 。
You don’t use the Version property on the Entity base class with RavenDB because it already has a version tag built in by default. When loading a document from RavenDB, it caches the Etag that relates to it. The Etag is basically the version stamp. When the call to commit the session is made, RavenDB checks to see if the Etag has been updated since retrieving the document. If it has, the ConcurrencyException is thrown.
CreateAuction应用程序服务很简单,它与 NHibernate 实现类似,如清单 21-73 所示。
The CreateAuction application service is simple, and it’s similar to the NHibernate implementation, as displayed in Listing 21-73.
因为现在在Auction实体上有了公共属性,所以可以将它们转换为查询服务返回的 DTO。首先创建AuctionStatusQuery,如清单 21-74 所示。
Because you now have public properties on the Auction entity, you can transform them into the DTOs that the query services return. First create the AuctionStatusQuery with Listing 21-74.
返回给定拍卖的出价的查询使用BidHistory对象来确保出价的排序正确,因为这是领域逻辑。将BidHistoryQuery类添加到清单 21-75 的解决方案中。
The query to return the bids for a given auction uses the BidHistory object to ensure the ordering of the bids is correct because this is domain logic. Add the BidHistoryQuery class to the solution with Listing 21-75.
您可能还记得,IBidHistoryRepository合约公开了拍卖的出价数量。为了支持这一点,您可以创建一个索引来改进对这些数据的查询。将清单 21-76 中的索引类BidHistory_NumberOfBids添加到Infrastructure文件夹中。
As you will remember, the IBidHistoryRepository contract exposes a count of the number of bids against an auction. To support this, you create an index to improve querying for this data. Add the index class BidHistory_NumberOfBids with Listing 21-76 to the Infrastructure folder.
BidHistoryRepository实现(清单 21-77)与 NHibernate 实现类似,只是使用了之前创建的索引来返回拍卖的出价数量。请注意,在查询中,您正在调用自定义WaitForNonStaleResultsAsOfNow因为 RavenDB 最终是一致的,并且您正在阻止线程以确保索引是最新的。
The BidHistoryRepository implementation, Listing 21-77, is similar to the NHibernate implementation apart from the use of the index created earlier to return the number of bids placed against an auction. Notice that in the query, you are calling the customization WaitForNonStaleResultsAsOfNow because RavenDB is eventually consistent and you are blocking the thread to ensure the index is up to date.
IAuctionRepository的实现很简单,如清单 21-78 所示。
The implementation of the IAuctionRepository is straightforward and is shown in Listing 21-78.
您将使用 StructureMap 连接应用程序服务的依赖项。使用 NuGet 安装 StructureMap 库,就像在 NHibernate 示例中所做的那样。然后将以下Bootstrappper类(清单 21-79)添加到应用程序类库项目的路由中。
You will use StructureMap to wire up the dependencies of your application service. Install the StructureMap libraries, as you did for the NHibernate example, using NuGet. Then add the following Bootstrappper class, Listing 21-79, to the route of the application class library project.
最后,在演示控制台项目的app.config文件中需要进行一个小配置,如清单 21-80 所示。
Lastly, there is a small configuration that is required in the app.config file of the Presentation console project, as shown in Listing 21-80.
Presentation控制台项目中的Program类文件与 NHibernate 版本相同。运行示例程序后,您可以启动 RavenDB 的管理工作室并检查拍卖文档,如图21.17所示。
The Program class file within the Presentation console project is the same as the NHibernate version. Once you run the sample program, you can launch RavenDB’s management studio and inspect the auction document, as shown in Figure 21.17.
图 21.17检查 RavenDB Management Studio 中的文档。
FIGURE 21.17 Inspecting the document inside RavenDB Management Studio.
对于无法将领域模型直接映射到数据模型的持久化框架,你需要采用不同的持久化方法。确保你的数据模型不会影响领域模型的结构,并且领域模型可以干净利落地演进。您将进行两个练习:一个使用实体框架 (Entity Framework),另一个使用原始 ADO.NET,并借助微型 ORM Dapper 进行操作。
For the persistence frameworks that are unable to map a domain model directly to the data model, you need to take a different approach to persistence. It is still important to ensure that your data model does not affect the structure of your domain model and that your domain model can evolve cleanly. You will run through two exercises: one using Entity Framework and the other raw ADO.NET with some help from Dapper, a micro ORM.
实体框架 (Entity Framework) 是微软的企业级 ORM。它的功能与 NHibernate 类似,但 NHibernate 框架更成熟,拥有更多直接映射领域模型的功能。在本例中,您将使用实体框架仅映射数据模型,并利用备忘录模式提供领域模型状态的快照以供存储。
The Entity Framework is Microsoft’s enterprise-level ORM. It has similar capabilities to NHibernate, but it’s fair to say that the NHibernate framework is a more mature project with more features for mapping a domain model directly. In this example, you use Entity Framework to map only the data model and utilize the memento pattern to provide a snapshot of the state of the domain model for storage.
创建一个名为DDDPPP.Chap21.EFExample的新空白 Visual Studio 解决方案,并向其中添加一个名为DDDPPP.Chap21.EFExample.Application的类库和一个名为DDDPPP.Chap21.EFExample.Presentation的控制台应用程序。在Presentation项目中,添加对该应用程序的引用。您可以删除自动创建的Class1.cs文件,因为您不需要它。使用 NuGet 安装实体框架客户端库,如图21.18所示。
Create a new blank Visual Studio solution named DDDPPP.Chap21.EFExample and add to this a class library named DDDPPP.Chap21.EFExample.Application and a console application named DDDPPP.Chap21.EFExample.Presentation. Within the Presentation project, add a reference to the application. You can delete the Class1.cs that is automatically created because you won’t be needing it. Use NuGet to install the Entity Framework client libraries, as shown in Figure 21.18.
图 21.18通过 NuGet 安装实体框架客户端库。
FIGURE 21.18 Install Entity Framework client libraries via NuGet.
按照与 NHibernate 示例相同的方式构建应用程序,或者直接将类文件复制到这个新的解决方案中,并确保更新命名空间。删除以下特定于 NHibernate 示例的类:
Build out the application in the same manner as you did for the NHibernate example, or simply copy the class files into this new solution, ensuring that you update the namespaces. Remove the following classes that were specific to the NHibernate example:
您将得到类似于图 21.19 的解决方案。
You are left with a solution that resembles Figure 21.19.
图 21.19解决方案资源管理器中有一个基于 NHibernate 示例的应用程序。
FIGURE 21.19 Solution explorer with an application based on the NHibernate sample.
此外,如果您尚未创建数据库,请使用与 NHibernate 示例相同的模式创建数据库表。
Also, create the database tables using the same schema you used for the NHibernate example, if you haven’t already created the database.
因为您将要实现备忘录模式,所以您需要聚合生成自身的快照,以便将其映射到数据模型。您将创建的第一个快照是WinningBid值对象。使用清单 21-81,在Model 文件夹内的Auction文件夹中添加一个名为WinningBidSnapshot的新类。
Because you are going to implement the memento pattern, you need the aggregate to produce a snapshot of itself so you can map it to a data model. The first snapshot you will create is of the WinningBid value object. Using Listing 21-81, add a new class to the Auction folder within the Model folder named WinningBidSnapshot.
接下来,为拍卖本身创建一个快照,如清单 21-82 所示。它保存了WinningBidSnapshot并提供拍卖聚合的完整快照。
Next, create a snapshot for the auction itself, as shown in Listing 21-82. This holds the WinningBidSnapshot and provides a full snapshot of the Auction aggregate.
最后,为金钱值对象的状态创建一个快照,如清单21-83所示。
Finally, create a snapshot for the state of the money value object, as shown in Listing 21-83.
因为您需要访问Version和Id属性的 setter 和 getter,所以更新可访问性级别,以便它们具有受保护的 setter 和公共 getter,如清单 21-84 所示。
Because you need access to setters and getters of the Version and Id properties, update the accessibility levels so that they have protected setters and public getters, as shown in Listing 21-84.
要提取拍卖聚合中域对象的状态,需要添加一个返回快照的新公共方法。添加此方法的第一个对象是Money值对象。更新Money类以包含新的GetSnapshot方法,如清单 21-85 所示。
To extract the state of the domain objects within the auction aggregate, you need to add a new public method that returns a snapshot. The first object to add this method to is the Money value object. Update the Money class to include the new GetSnapshot method, as shown in Listing 21-85.
对WinningBid对象执行相同的操作,但对于这种更复杂的类型,添加一个新的静态工厂方法,使WinningBid能够从快照创建,如清单 21-86 所示。
Do the same for the WinningBid object, but for this more complex type, add a new static factory method that enables a WinningBid to be created from a snapshot, as shown in Listing 21-86.
最后,通过添加静态工厂类来更新Auction类,以便从快照中补充拍卖,并添加获取快照的方法,如清单 21-87 所示。
Finally, update the Auction class by adding the static factory class to hydrate an auction from a snapshot and the method to obtain a snapshot, as shown in Listing 21-87.
您还需要添加BidHistory对象,就像在 RavenDB 示例中所做的那样。清单 21-88 展示了一个示例。
You also need to add the BidHistory object, as you did in the RavenDB example. Listing 21-88 shows an example.
要检索BidHistory对象,需要修改IBidHistoryRepository,如清单 21-89 所示。
To retrieve the BidHistory object, you need to amend the IBidHistoryRepository, as shown in Listing 21-89.
因为您无法使用域对象隐式跟踪更改,所以您需要显式保存它们,这意味着您必须向IAuctionRepository添加一个保存方法,如清单 21-90 所示。
Because you can’t track changes implicitly with your domain objects, you need to explicitly save them, which means you must add a save method to the IAuctionRepository, as shown in Listing 21-90.
在Infrastructure文件夹中创建一个名为DataModel的新文件夹。在这里,您将暂停映射到数据库表和行的数据模型对象。要创建的第一个对象表示Auction表中的一行,如清单 21-91 所示。
Create a new folder within the Infrastructure folder named DataModel. Here you stall the data model objects that map to your database tables and rows. The first object to create represents a row within the Auction table, as shown in Listing 21-91.
第二个数据对象,清单 21-92,代表BidHistory表中的一行。
The second data object, Listing 21-92, represents a row within the BidHistory table.
您将使用 Entity Framework 的代码优先映射将数据模型映射到数据库。向Infrastructure文件夹中添加一个名为Mapping的新文件夹。清单 21-93 中的AuctionMap类将AuctionDTO映射到Auctions数据库表。
You will use Entity Framework’s code-first mapping to map the data model to the database. Add a new folder to the Infrastructure folder named Mapping. The AuctionMap class, Listing 21-93, maps the AuctionDTO to the Auctions database table.
类似地,清单 21-94 中的BidMap将BidDTO映射到BidHistory表。
Similarly, the BidMap, Listing 21-94, maps the BidDTO to the BidHistory table.
要与数据库通信,需要一个上下文,如清单 21-95 所示。Entity Framework 的DbContext类似于 NHibernate 的ISession和 RavenDB 的IDocumentSession。它实际上是 Entity Framework 对工作单元模式的实现。
To talk to the database, you need a context, as shown in Listing 21-95. The Entity Framework DbContext is similar to NHibernate’s ISession and RavenDB’s IDocumentSession. It is effectively Entity Framework’s implementation of the unit-of-work pattern.
存储库的实现与您在NHibernate和 RavenDB 示例中看到的不同。由于无法将域模型直接映射到数据模型,因此存储库需要提取快照并进行映射。AuctionRepository实现如清单 21-96 所示。
The implementation of the repository is different from what you have seen in the NHibernate and RavenDB examples. Because you are unable to map the domain model directly to the data model, the repository needs to extract the snapshot and map that instead. The AuctionRepository implementation is shown in Listing 21-96.
BidHistoryRepository的工作方式与清单 21-97 中所示的相同。
The BidHistoryRepository works along the same lines as what’s shown in Listing 21-97.
除了使用DbContext(Entity Framework 的工作单元实现)之外,这两个应用服务与您目前所见的并没有太大区别。唯一值得注意的是,Entity Framework 在事务中隐式地包装了对SaveChanges的调用,因此您不需要这样做。您可以在清单 21-98 中看到这些变化。
There isn’t much difference in either of the application services from what you have seen up to now except for using the DbContext, which is Entity Framework’s unit of work implementation. The only thing of note is to say that Entity Framework implicitly wraps the call to SaveChanges in a transaction so you don’t need to. You can see these changes in Listing 21-98.
在BidOnAuction应用服务中,如清单 21-99 所示,您需要显式调用存储库上的Save方法。唯一的区别是,出现并发问题时抛出的异常类型与 NHibernate 和 RavenDB 示例不同。
In the BidOnAuction application service, Listing 21-99, you need to explicitly call the Save method on the repository. The only other difference is that the exception type that is thrown when there is a concurrency issue is different from both the NHibernate and RavenDB examples.
要实现查询,您可以利用快照来填充视图模型,如清单 21-100 和清单 21-101 中的AuctionStatusQuery和BidHistoryQuery所示。
To implement the queries, you can utilize the snapshots to populate the view models, as shown in Listing 21-100 and Listing 21-101 for the AuctionStatusQuery and the BidHistoryQuery.
再次,您将使用 StructureMap 来连接您的依赖项,因此像以前一样从 NuGet 安装它,并创建一个引导程序类,如清单 21-102 所示。
Again, you will be using StructureMap to wire up your dependencies, so install it as before from NuGet and create a bootstrapper class with Listing 21-102.
最后,将清单 21-103 中的 XML 代码片段添加到演示控制台应用程序中的app.config中。
Lastly, add the XML snippet in Listing 21-103 to the app.config within the Presentation console application.
如果您运行控制台应用程序,您将看到与前面的 NHibernate 和 RavenDB 示例相同的结果。
If you run the console application, you will see the same results as in the previous NHibernate and RavenDB examples.
Dapper 是一个微型 ORM,这意味着它能帮你将数据库行映射到对象,但(根据设计)几乎没有其他功能。在本例中,你需要构建自己的工作单元实现和并发检查。
Dapper is a micro ORM, meaning that it helps you map database rows to objects but (by design) does little else. In this example, you need to build your own unit of work implementation and concurrency checks.
首先,创建一个名为DDDPPP.Chap21.MicroORM的新空白 Visual Studio 解决方案,并在其中添加一个名为DDDPPP.Chap21.MicroORM.Application的类库和一个名为DDDPPP.Chap21.MicroORM.Presentation的控制台应用程序。在Presentation项目中,添加对该应用程序的引用。您可以删除自动创建的Class1.cs文件,因为您不需要它。使用 NuGet 安装 Dapper 客户端库,如图21.20所示。
To get started, create a new blank Visual Studio solution named DDDPPP.Chap21.MicroORM and add to this a class library named DDDPPP.Chap21.MicroORM.Application and a console application named DDDPPP.Chap21.MicroORM.Presentation. Within the Presentation project, add a reference to the application. You can delete the Class1.cs that is automatically created because you won't be needing it. Use NuGet to install the Dapper client libraries, as shown in Figure 21.20.
图 21.20通过 NuGet 安装 Dapper 客户端库。
FIGURE 21.20 Install Dapper client libraries via NuGet.
该模型与您为 Entity Framework 示例创建的模型相同。它还具有您在 NHibernate 和 Entity Framework 示例中使用的数据库架构。以与 Entity Framework 相同的方式构建应用程序,但不包括以下文件:
The model is the same as the model you created for the Entity Framework example. It also has the database schema that you used in both the NHibernate and Entity Framework examples. Build out the application in the same manner as you did for the Entity Framework, but exclude the following files:
现在您应该得到类似于图 21.21的解决方案。
You should now be left with a solution that resembles Figure 21.21.
图 21.21基于实体框架解决方案的骨架解决方案。
FIGURE 21.21 Skeleton solution based on the Entity Framework solution.
在本例中,我们使用微型 ORM。微型 ORM 往往注重简洁性和性能,因此其功能比全自动 ORM(Entity Framework 和 NHibernate)要少。因此,我们需要自行实现工作单元模式。本例中的工作单元结构基于 Tim McCarthy 在其著作《.NET 领域驱动设计与 C#:问题-设计-解决方案》中使用的框架。
In this example we’re using a micro ORM. Micro orms tend to focus on simplicity and performance, as such they have less features than their fully automated cousins Entity Framework and NHibernate. Consequently we will need to implement the unit of work pattern ourselves. The unit of work structure in this example is based on the framework that Tim McCarthy uses in his book .NET Domain-Driven Design with C#: Problem-Design-Solution.
清单 21-104 中的IAggregateDataModel接口实际上本身就是一种模式,称为标记接口模式。该接口充当类的元数据,与该类实例交互的方法在执行其工作之前会测试接口是否存在。在本章后面构建仅持久化实现IAggregateDataModel接口的业务对象的存储库层时,您将看到此模式的使用。
The IAggregateDataModel interface, Listing 21-104, is actually a pattern in itself called the marker interface pattern. The interface acts as metadata for a class, and methods that interact with instances of that class test for the existence of the interface before carrying out their work. You will see this pattern used later in this chapter when you build a repository layer that only persists business objects that implement the IAggregateDataModel interface.
工作单元实现使用IAggregateDataModel接口来引用参与原子事务的任何业务实体。向Infrastructure项目添加另一个名为IUnitOfWorkRepository的接口,如清单 21-105 所示,其契约清单如下。
The unit of work implementation uses the IAggregateDataModel interface to reference any business entity that is partaking in an atomic transaction. Add another interface to the Infrastructure project named IUnitOfWorkRepository, Listing 21-105, with the contract listing that follows.
IUnitOfWorkRepository是所有存储库(如果打算在工作单元中使用)都必须实现的第二个接口。您可以将此契约定义添加到稍后要添加的模型存储库接口中,但这两个接口分别针对两种不同类型的关注点。这就是接口隔离原则的定义。您不会在应用程序中删除任何内容,因此无需添加该方法。
The IUnitOfWorkRepository is a second interface that all repositories are required to implement if they intend to be used in a unit of work. You could have added this contract definition to the model Repository interface that you will add later, but the interfaces are addressing two different types of concerns. This is the definition of the Interface Segregation Principle. You are not going to be deleting anything in your application, so there is no need to add that method.
最后,向基础设施项目添加第三个接口,名为IUnitOfWork,其定义可以在清单 21-106 中找到。
Finally, add a third interface to the Infrastructure project named IUnitOfWork, the definition of which you can find in Listing 21-106.
IUnitOfWork接口在注册修改/添加/删除操作时需要IUnitOfWorkRepository接口,这样,在提交操作后,工作单元就可以将实际持久化方法的工作委托给相应的具体实现。当您查看IUnitOfWork接口的默认实现并创建存储库时, IUnitOfWork方法背后的逻辑将变得更加清晰。
The IUnitOfWork interface requires the IUnitOfWorkRepository when registering an amend/ addition/deletion so that, on commitment, the unit of work can delegate the work of the actual persistence method to the appropriate concrete implementation. The logic behind the IUnitOfWork methods will become a lot clearer when you look at a default implementation of the IUnitOfWork interface and create the repositories.
最后要创建的类是ConcurrencyException,如清单 21-107 所示,如果在检索、出价和保存拍卖之间更新拍卖,则会抛出该异常。
The last class to create is the ConcurrencyException, Listing 21-107, which is thrown if the auction is updated between retrieving it, placing a bid, and persisting it.
现在您应该对这两个应用服务都很熟悉了。清单 21-108 和清单 21-109 会根据工作单元的实现和并发异常类型而发生变化。
Both of the application services should be familiar to you by now. Listing 21-108 and Listing 21-109 change because of your implementation of the unit of work and because of your concurrency exception type.
您需要使AuctionDTO(清单 21-110)和BidDTO(清单 21-111)实现IAggregateDataModel接口,因为这两个类是使用工作单元实现来持久化的。
You need to make the AuctionDTO, Listing 21-110, and BidDTO, Listing 21-111, implement the IAggregateDataModel interface because these two classes are persisted using the unit of work implementation.
如清单 21-112 和清单 21-113 所示, AuctionRepository和BidHistoryRepository都实现了领域模型存储库契约IAuctionRepository和IBidHistoryRepository,以及IUnitOfWorkRepository接口。这两个存储库方法的实现只是将工作委托给工作单元,将要持久化的实体与对存储库的引用一起传递,存储库当然实现了IUnitOfWorkRepository。如前所述,当调用工作单元的Commit方法时,工作单元引用存储库的IUnitOfWorkRepository契约实现来执行真正的持久化需求。这与您在企业级框架中看到的行为相同。由于存储库需要从app.config文件获取连接字符串,因此需要添加对Systems.Configuration程序集的引用,如图 21.22所示。
Both AuctionRepository and BidHistoryRepository, shown in Listing 21-112 and Listing 21-113, implement the domain model repository contracts IAuctionRepository and IBidHistoryRepository, as well as the IUnitOfWorkRepository interface. The implementations of both repository methods simply delegate work to the unit of work, passing the entity to be persisted along with a reference to the repository, which of course implements the IUnitOfWorkRepository. As seen previously when the unit of work’s Commit method is called, the unit of work refers to the repository’s implementation of the IUnitOfWorkRepository contract to perform the real persistence requirements. This is the same behavior as you saw in the enterprise-level frameworks. Because the repositories need to obtain a connection string from the app.config file, you need to add a reference to the Systems.Configuration assembly, as shown in Figure 21.22.
图 21.22添加对 System.Configuration 的引用。
FIGURE 21.22 Add a reference to System.Configuration.
要完成存储库实现,需要实现工作单元接口。向Infrastructure文件夹中添加一个名为UnitOfWork的新类,并使用清单 21-113 更新新创建的类。
To complete the repository implementation, you need to implement the unit-of-work interface. Add a new class to the Infrastructure folder named UnitOfWork, and use Listing 21-113 to update the newly created class.
您需要添加对System.Transactions 的引用,以便可以使用TransactionScope类,如图 21.23所示,这确保持久性将在原子事务中提交。
You are required to add a reference to System.Transactions so you can use the TransactionScope class, as shown in Figure 21.23, which ensures the persistence will commit in an atomic transaction.
图 21.23添加对 System.Transactions 的引用。
FIGURE 21.23 Add a reference to System.Transactions.
UnitOfWork类使用三个字典来跟踪业务实体的待处理更改。第一个字典对应要添加到数据存储区的实体。第二个字典跟踪要更新的实体,第三个字典处理实体的移除。一个匹配的IUnitOfWorkRepository存储在字典中的实体键对应的位置,并在Commit方法中用于调用存储库,该存储库包含实际持久化实体的代码。Commit方法循环遍历每个字典,并调用相应的IUnitOfWorkRepository方法,传递对实体的引用。Commit 方法中的工作使用块包装在TransactionScope中;这确保在调用TransactionScope Complete方法之前不会执行任何工作。如果在IUnitOfWorkRepository中执行工作时发生异常,则所有工作都会回滚,数据存储区将保持其原始状态。
The UnitOfWork class uses three dictionaries to track pending changes to business entities. The first dictionary corresponds to entities to be added to the datastore. The second dictionary tracks entities to be updated, and the third deals with entity removal. A matching IUnitOfWorkRepository is stored against the entity key in the dictionary and is used in the Commit method to call the repository, which contains the code to actually persist an entity. The Commit method loops through each dictionary and calls the appropriate IUnitOfWorkRepository method, passing a reference to the entity. The work in the Commit method is wrapped in a TransactionScope using a block; this ensures that no work is done until the TransactionScope Complete method is called. If an exception occurs while you are performing work within the IUnitOfWorkRepository, all work is rolled back, and the datastore is left in its original state.
再次,您将使用 StructureMap 连接应用程序服务的依赖项,您可以通过 NuGet 安装它,就像安装 NHibernate 和 RavenDB 一样。代码如清单 21-115 所示。
Again, you will wire up the dependencies of the application services using StructureMap, which you can install via NuGet just as you did with NHibernate and RavenDB. The code is shown in Listing 21-115.
要设置到数据库的连接字符串,请更新演示项目中的App.Config以匹配清单 21-116 中的 XML 标记。
To set the connection string to the database, update the App.Config within the presentation project to match the XML markup in Listing 21-116.
现在您可以运行该程序并看到与所有示例相同的结果。
You can now run the program and see the same results, as in all the examples.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 22 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 22 download and individually named according to the names throughout the chapter.
事件溯源 (Event Source) 不仅能带来竞争优势,还能应对激动人心的技术挑战,因此近年来,一种名为事件溯源的存储机制广受欢迎。由于存储了完整的活动历史记录,事件溯源使企业能够深入了解其数据的方方面面,包括客户的详细行为。借助这些历史信息,企业可以提出新颖的查询,为产品开发、营销策略和其他业务决策提供信息。借助事件溯源,您可以确定系统在任何给定时间点的状态以及它如何达到这些状态。对于许多领域来说,这是一种改变游戏规则的能力。
Bringing a competitive business advantage and exciting technical challenges, it’s clear why a storage mechanism called event sourcing has gained a lot of popularity in recent years. Because a full history of activity is stored, event sourcing allows businesses to deeply understand many aspects of their data, including detailed behavior of their customers. With this historical information, new and novel queries can be asked that inform product development, marketing strategies, and other business decisions. Using event sourcing, you can determine what the state of the system looked like at any given point in time and how it reached any of those states. For many domains, this is a game-changing capability.
如今,许多系统仅存储领域模型的当前状态,从而剥夺了分析历史行为的机会。因此,事件溯源的基础是新的数据存储方式和构建领域模型的新方法,这些方法本身也存在挑战和优势。本章将逐步向您介绍许多此类场景的具体示例。您甚至还将了解如何构建事件存储,并了解专门构建的事件存储。
Many systems today store only the current state of the domain model, thereby precluding the opportunity to analyze historical behavior. So, fundamental to event sourcing are new ways of storing data and new ways of building domain models with inherent challenges and advantages. This chapter takes you through concrete step-by-step examples of many such scenarios. You will even see how to build an Event Store and be introduced to a purpose-built Event Store.
领域驱动设计 (DDD) 从业者通常喜欢将事件溯源与 CQRS 相结合,以此作为增强可扩展性和性能的基础。CQRS 本质上是关于非规范化的,但其与事件溯源的协同作用如此之强,以至于围绕这一组合已经形成了一个完整的社区。这使得理解和消除这些概念的歧义变得至关重要。鉴于社区中各种炒作,务实的态度也至关重要。因此,本章将帮助您了解在哪些情况下事件溯源可能并非一个有效的选择。
Domain-Driven Design (DDD) practitioners often like to combine event sourcing with CQRS as the basis for enhanced scalability and performance. CQRS is fundamentally about denormalization, but such is the synergy with event sourcing that a whole community has formed around the combination. This makes understanding and disambiguating the concepts highly important. It’s also important to be realistic in light of all the hype generated by the community. So this chapter helps you understand when event sourcing might not be an efficient choice.
为了准备技术示例,本章首先通过与传统的仅存储当前状态的存储进行比较,强调了事件溯源的重要性。本章中的所有其他示例均基于一家手机网络运营商提供的即用即付服务。通过这项服务,客户可以使用代金券或充值卡充值账户余额,从而增加信用额度。信用额度允许他们拨打电话。
In preparation for the technical examples, this chapter begins by accentuating the importance of event sourcing using comparisons with traditional current-state-only storage. This, and all the other examples in the chapter, are based on a pay-as-you-go service offered by a cell phone network operator. With this service, customers add credit to their account by topping-up their balance with vouchers or Top-Up cards. Credit allows them to make phone calls.
如果仅存储域模型的当前状态,则无法了解系统如何达到该状态。因此,您无法分析过去的行为以发现新的见解或找出出了什么问题。这可以通过检查提交给手机网络运营商客户服务部门的支持案例来证明。一位客户报告说,她对最近充值后余额耗尽感到不满,之后只打了几个简短的电话。她确信自己应该还有余额。通过查看数据库,客户服务助理提取了客户的帐户信息,如表 22.1所示。
If you only store the current state of your domain model, you don’t have a way of understanding how the system reached that state. Consequently, you cannot analyze past behavior to uncover new insights or to work out what has gone wrong. This can be demonstrated by examining a support case presented to the customer service department of a cell phone network operator. A customer reported her dissatisfaction at having an empty balance after she recently topped up and then made only a few short phone calls. She is convinced that she should still have remaining credit. By looking at the database, the customer service assistant pulls up the customer’s account information that is shown in Table 22.1.
表 22.1客户的现收现付账户活动
TABLE 22.1 Customer’s Pay-as-You-Go Account Activity
| 客户 ID | 津贴(分钟) |
| 123456789 | 0 |
查看表 22.1中的客户账户,客服助理所能做的就是承认账户余额为空。通过查看最近的通话和充值详情,无法查明账户为何会变成这样。客户利用手机通话记录和充值收据,提供了账户历史记录来佐证自己的观点,如表 22.2所示。
Looking at the customer’s account in Table 22.1, all the customer service assistant can do is agree that the account balance is empty. It’s not possible to work out how the account reached this state by viewing recent call and top-up details. Using the logs from her phone and receipts for her top-ups, the customer supports her argument by providing the history of her account, as shown in Table 22.2.
表 22.2客户实际账户活动
TABLE 22.2 Customer’s Actual Account Activity
| 先前津贴(分钟) | 行动 | 当前限额(分钟) |
| 0 | 充值$10 | 20 |
| 20 | 10分钟通话 | 10 |
| 15 | 5分钟通话 | 0 |
如果你观察表 22.2,就会发现 10 美元可以购买 20 分钟的通话时间。然而,错误的是,仅在 10 分钟和 5 分钟的通话后,余额就为 0。系统存在一个 bug,但商家无法仅通过查看数据轻易证明这一点,因为它只显示当前状态。它没有保留客户活动的历史记录。
If you observe Table 22.2, it follows that $10 buys 20 minutes of phone calls. Incorrectly, though, the balance is 0 after only a 10-minute and a 5-minute phone call. There is a bug in the system, but the business cannot easily prove it just by looking at its data because it only shows the current state. It does not keep a history of the customer’s activity.
通过将状态存储为快照,手机运营商也缺乏利用丰富的客户行为历史记录来改进服务的能力。它无法将用户行为与营销推广关联起来,也无法从用户活动中寻找可转化为新的或增强的收入来源的模式。
By storing state as a snapshot, the cell phone operator also lacks the ability to use the rich history of customer behavior to improve its services. It is unable to correlate user behavior with marketing promotions, and it is unable to look for patterns in user activity that can be capitalized on as new or enhanced streams of revenue.
在这种情况下,手机网络运营商至少应该考虑使用事件溯源来记录账户历史记录,这显然是一个商业案例。仅仅通过将其领域模型的状态存储为事件流,它就可能获得竞争优势并减少客户服务投诉。
In this scenario, there is a clear business case for the cell phone network operator to at least consider using event sourcing as a way to record account history. It might be able to gain a competitive advantage and a reduction in customer service complaints just by storing the state of its domain model as a stream of events.
您可以通过按时间顺序存储每个重要事件及其时间戳来获得分析完整历史数据的能力。然后,您可以通过重放事件来获取当前状态。然而,更重要的是,您不仅可以计算当前状态;您还可以重放任何事件的后续序列,以计算出历史中任意时间点的状态以及导致该状态的活动。这被称为时间查询——事件溯源固有的颠覆性功能。
You can acquire the ability to analyze full historical data by storing each event of significance in chronological order with its timestamp. You then derive current state by replaying events. Significantly, though, you can do more than just work out the current state; you can replay any subsequence of events to work out the state, and activity that caused it, for any point in history. This is known as a temporal query—a game-changing capability inherent to event sourcing.
时态查询就像是时光倒流的能力,因为本质上,你可以将领域模型的状态回溯到历史的某个时间点。使用时态查询,客户服务部门可以准确评估客户对错误余额的投诉。它可以反复检查先前时间点的状态,直到找到错误地从限额中扣除双倍金额的事件。图 22.1说明了如何将时态查询应用于表示客户预付账户活动的事件流,该活动在表 22.1中有详细说明。
Temporal queries are like the ability to travel back in time, because essentially, you can rewind the state of your domain model to a previous point in history. Using a temporal query, the Customer Service department could accurately assess its customer’s complaints about an incorrect balance. It could repeatedly check the state at previous points in time until it found the event that incorrectly deducted double the amount from the allowance. Figure 22.1 illustrates how temporal queries could be applied to the event stream representing the customer’s pay-as-you-go account activity detailed previously in Table 22.1.
图 22.1通过重放事件来计算历史上任意一点的状态。
FIGURE 22.1 Calculating the state for any point in history by replaying events.
重放事件是时态查询的基础机制。图 22.1展示了如何使用事件子序列来计算客户账户在两个不同历史时刻的状态。在您的开发生涯中,您一定接触过类似的时态查询。例如,银行账户或版本控制系统 (VCS),例如 Git 或 Subversion。利用这些概念,您可以通过重放之前发生的所有事件,将状态回溯到历史中的任意时刻。在本章后面,您将看到针对真实事件存储执行时态查询的示例。
Replaying events is the mechanism that underlies temporal queries. Figure 22.1 illustrates how subsequences of events are used to calculate the state of the customer’s account at two different points in history. Throughout your life and career as a developer, you have been exposed to temporal queries similar to these. Examples include bank accounts or version control systems (VCSs) like Git or Subversion. With each of these concepts, you can rewind state to any point in history by replaying all the events that occurred prior to it. Later in this chapter, you will see examples of performing temporal queries against a real Event Store.
利用图 22.1中的事件流,手机运营商现在可以询问有关账户历史记录的任何问题。更有趣的是,运营商还可以整合来自众多客户账户的信息,从而推算出特定人群的行为。这些信息可用于制定定量营销和产品开发决策,或指导实验。投影是实现整合来自多个流的事件以执行此类复杂时间查询的底层功能。
By taking advantage of the event stream in Figure 22.1, the cell phone operator can now ask any question about the history of the account. But, even more interestingly, the operator can also combine information from many customers’ accounts to work out behaviors of specific demographics. Such information can be used to make quantitative marketing and product development decisions or guide experimentation. Projections are the underlying feature that enables combining events from multiple streams to carry out these kinds of complex temporal queries.
某些事件存储的限制是难以针对多个事件流执行临时查询,而在 SQL 数据库中,这可以通过简单的连接实现。为了解决这个问题,事件存储使用一个称为投影的概念,它是将一组输入事件流映射到一个或多个新输出流的查询。例如,手机运营商可以为其许多客户投影事件流,以回答如下问题:“在某个体育赛事期间,特定人群在特定日期的总使用时间是多少分钟”和“当有特别优惠时,特定人群的总使用量是增加还是减少了?”图 22.2说明了如何通过使用投影组合来自多个流的事件来提出这样的任意问题。
A limitation of some event stores is a difficulty in carrying out ad hoc queries against multiple event streams, which in a SQL database would be achievable with straightforward joins. To get around this problem, event stores use a concept called projections, which are queries that map a set of input event streams onto one or more new output streams. For example, a cell phone operator can project the event streams for many of its customers to answer questions like these: “What was the total number of minutes used on a specific day by a specific demographic around the time of a certain sporting event” and “Did the total usage increase or decrease among a certain demographic when there was a special offer?” Figure 22.2 illustrates how arbitrary questions like these can be asked by combining events from multiple streams using projections.
图 22.2从多个事件流创建投影。
FIGURE 22.2 Creating projections from multiple event streams.
在图 22.2中,代表单个客户帐户的每个事件流都被输入到投影函数中。投影函数可以做很多事情,包括保持状态或发出新事件。图 22.2中的投影函数将把 3 月 1 日至 3 月 5 日之间发生的所有事件(账户持有人年龄在 16 至 24 岁之间)发送到新的事件流中。这个新流就是投影。将所有必需的事件都放在一个流中,然后可以轻松高效地查询所有事件以找出总数、平均值或任何其他需要提取的信息。
In Figure 22.2, each event stream that represents the account of a single customer is fed into the projection function. A projection function can do a number of things, including keeping state or emitting new events. The projection function in Figure 22.2 is going to emit all events that occurred between March 1 and March 5, and where the account holder is between 16 and 24 years of age, into a new event stream. This new stream is the projection. With all the required events in a single stream, it is then easy and efficient to query all of them to find out totals, averages, or any other piece of information that needs to be extracted.
您可以将一组输入流投影到多个输出。正如您将在本章后面看到的,使用 Greg Young 的 Event Store,您将获得强大的功能和灵活性。您可以在投影函数中使用 JavaScript 的丰富功能。
You can project a set of input streams onto more than one output. As you’ll see later in this chapter, with Greg Young’s Event Store, you have a lot of power and flexibility. You can use the rich capabilities of JavaScript in projection functions.
将状态存储为事件的后果是事件流可能会变得非常大,这意味着重放事件的时间可能会持续显著增加。为了避免这种性能损失,事件存储使用快照。如图 22.3所示,快照是事件流中的中间步骤,表示重放所有先前事件后的状态。
A consequence of storing state as events is that event streams can grow very large, meaning that the time to replay events can continue to increase significantly. To avoid this performance hit, event stores use snapshots. As Figure 22.3 illustrates, snapshots are intermediate steps in an event stream that represent the state after replaying all previous events.
图 22.3使用快照高效恢复状态。
FIGURE 22.3 Efficiently restoring state by using snapshots.
当应用程序想要从事件流中加载聚合的当前状态时,它只需找到最新的快照。然后,以该快照作为初始状态,它只需重放流中的所有后续事件。
When an application wants to load the current state of an aggregate from an event stream, all it has to do is find the latest snapshot. Using that snapshot as the initial state, it need then only replay all the subsequent events in the stream.
为了兼容事件溯源,聚合需要面向事件。重要的是,它们需要能够通过应用一系列事件来计算其状态。DDD 从业者发现,这样做的一个好处是,他们的聚合更加面向行为,从而提供了更高级别的领域事件表达能力。另一个令人满意的好处是,持久化往往是松耦合的,问题更少。
For compatibility with event sourcing, aggregates need to be event oriented. Importantly, they need to be able to calculate their state by applying a series of events. DDD practitioners find that a nice side effect of this is that their aggregates are more behavior oriented, providing heightened levels of domain event expressiveness. Another satisfying benefit is that persistence tends to be loosely coupled and less problematic.
为事件源领域模型创建聚合时,需要注意一些关键细节。其中,最重要的是聚合能够应用领域事件并根据相应的业务规则更新其状态。其次,需要维护未提交事件的列表,以便将它们持久化到事件存储中。第三,聚合需要维护其版本的记录,并提供创建快照和从快照中恢复的能力。
There are a few key details involved when creating aggregates for event-sourced domain models. Of these details, the most important is for an aggregate to be able to apply a domain event and update its state according to the appropriate business rule(s). Second, a list of uncommitted events needs to be maintained so that they can be persisted to an Event Store. Third, an aggregate needs to maintain a record of its version and provide the ability to create snapshots and restore from them.
创建一个基类通常很有用,如清单 22-1 所示,所有聚合都从该基类继承以封装共性。
It’s often useful to create a base class, as per Listing 22-1, which all your aggregates inherit from to encapsulate commonality.
Changes是未提交事件的集合,Version显然跟踪聚合的版本,稍后会对此进行更详细的解释。Apply ()接受一个DomainEvent 参数,聚合必须通过应用业务规则并更新状态来处理该参数。
Changes is the collection of uncommitted events, and obviously Version keeps track of the aggregate’s version, which will be explained in more detail shortly. Apply() takes a DomainEvent that aggregates must handle by applying business rules and updating state.
清单 22-2 展示了一个具体的(部分)聚合实现,它代表了一个客户的预付费账户。这说明了在Apply()之上可以进行分层的表达能力。
LISTING 22-2 shows a concrete (partial) aggregate implementation that represents a customer’s pay-as-you-go account. This illustrates the expressiveness that can be layered on top of Apply().
清单 22-2 中的Apply()实现使用了少量动态魔法,从而实现了高度表达的事件处理方法。在清单 22-2 中,这些处理程序是When()方法。When ()的每个重载都旨在以近乎声明的方式阅读,表达应应用哪些业务规则以及当每种类型的事件发生时状态应如何变化。Apply ()还会在每次应用事件时更新Version字段。这在确定聚合在执行操作之前是否已更改时很有用。聚合的版本是相对于聚合事件流的起始位置,传递给其Apply()的最后一个事件的序列号。
The implementation of Apply() in Listing 22-2 uses a tiny amount of dynamic magic that allows highly expressive event-handling methods. In Listing 22-2, these handlers are the When() methods. Each overload of When() is intended to read almost declaratively, expressing what business rules should apply and how the state should change when each type of event occurs. Apply() also updates the Version field each time an event is applied. This is helpful when working out whether an aggregate has changed before an operation is carried out on it. An aggregate’s version is the sequence number, relative to the start of the aggregate’s event stream, of the last event that has been passed into its Apply().
Apply()主要是一个实现细节,不会在聚合外部调用。它之所以是公有的,是为了使聚合能够被重新水化(稍后解释)。在聚合外部,服务仍然通过富有表现力的 API 进行通信,而不知道与事件相关的特性。清单 22-3 中更新后的PayAsYouGoAccount聚合中的TopUp()就是一个例子。
Apply() is mostly an implementation detail that won’t be called outside the aggregate. The reason it is public is to enable the aggregate to be rehydrated (explained shortly). Outside of an aggregate, services still communicate through expressive APIs, being unaware of event-related features. This is exemplified by TopUp() in the updated PayAsYouGoAccount aggregate shown in Listing 22-3.
清单 22-3 展示了外部领域和应用服务看到的聚合视图。它们并不知道该聚合使用了事件源;它们只会看到像TopUp()这样富有表现力的高级 API ,这些 API 清晰地表达了领域概念。从这个角度来看,它与非事件源聚合的相似性是相似的。
LISTING 22-3 shows the view of an aggregate that external domain and application services would see. They would not know that the aggregate used event sourcing; they would only see expressive high-level APIs like TopUp() that clearly express domain concepts. From this perspective, the similarity to non-event-sourced aggregates is similar.
为了澄清事件主要是一个实现细节,与服务层聚合的交互也类似,清单 22-4 展示了TopUpCredit应用服务。请尝试关注它如何没有事件源的痕迹,以及它如何与非事件源聚合的交互几乎完全相同。
To clarify that events are mostly an implementation detail and that interaction with an aggregate from the service layer will be similar, Listing 22-4 presents the TopUpCredit application service. Try to focus on how there is no trace of event sourcing and how this would look almost identical to interaction with a non-event-sourced aggregate.
调用TopUp()时, CreditSatisfiedFreeCallAllowanceOffer或CreditAdded域事件将根据InclusiveMinutesOfferBusiness策略触发。重要的是,这些操作的结果将引发一个新事件。此新事件在传入Apply()后,需要更新聚合的状态。此外,该事件也需要持久化,以便在聚合从其事件流加载时随时重放。Causes ()通过将事件添加到Changes(未提交事件的集合)中,然后将事件传入Apply()来解决这些问题。
When TopUp() is invoked, either the CreditSatisfiedFreeCallAllowanceOffer or the CreditAdded domain event will occur based on the InclusiveMinutesOfferBusiness policy. Importantly, the outcome of these actions will result in a new event being raised. This new event will need to cause an update of the aggregate’s state, after being passed into Apply(). But the event will also need to be persisted so that it can be replayed anytime the aggregate is loaded from its event stream. Causes() takes care of these concerns by adding the event to Changes—the collection of uncommitted events—and then feeding the event into Apply().
在解决持久性问题之前,还有一个需要解决的聚合细节是创建快照的能力。聚合本身需要创建快照,因为它们包含通过重放所有先前事件来构建状态的领域逻辑。因此,在聚合上添加一个构建快照的方法和一个恢复聚合的方法会很方便。清单 22-5 显示了每个方法的示例。
One remaining aggregate detail that needs to be addressed before persistence can be tackled is the ability to create snapshots. The aggregates themselves need to create the snapshots, because they contain the domain logic that builds state by replaying all previous events. Accordingly, it’s convenient to add a method onto aggregates that builds a snapshot and one that restores the aggregate. An example of each is shown in Listing 22-5.
在清单 22-5 中,接受PayAsYouGoSnapshot参数的构造函数重载将聚合的状态恢复到与PayAsYouGoAccountSnapshot的状态匹配。如前所述,这是一个提高性能的快捷方式,相当于应用快照创建之前发生的所有事件。然而,要使其正常工作,快照需要准确地表示聚合的状态。您可以在清单 22-5 中看到,GetPayAsYouGoAccountSnapshot()通过创建快照并使用正确的值填充它来确保这一点。PayAsYouGoAccountSnapshot如清单22-6 所示。
In Listing 22-5, the constructor overload that takes a PayAsYouGoSnapshot restores the state of the aggregate to match the state of the PayAsYouGoAccountSnapshot. As previously discussed, this is a performance-improving shortcut that is equivalent to applying all the events that occurred before the snapshot was created. For this to work, though, snapshots need to accurately represent an aggregate’s state. You can see in Listing 22-5 that GetPayAsYouGoAccountSnapshot() ensures this happens by creating a snapshot and using the correct values to populate it. The PayAsYouGoAccountSnapshot is shown in Listing 22-6.
持久化事件源聚合只是将未提交的更改存储在事件存储中的一种情况。同样,加载聚合(也称为rehydrating)需要加载并重放所有先前存储的事件,并可以选择使用快照作为快捷方式。在前面的示例中,您看到Changes和Apply()分别用于这些目的。
Persisting event-sourced aggregates is just a case of storing the uncommitted changes in an event store. Similarly, loading the aggregate, also known as rehydrating, requires you to load and replay all previously stored events, with the option to use snapshots as a shortcut. You saw in the previous examples that Changes and Apply() are used for these purposes, respectively.
清单 22-7 包含一个示例存储库实现,用于处理加载和保存PayAsYouGoAccount聚合(最初忽略快照)。
LISTING 22-7 contains an example repository implementation that handles loading and saving PayAsYouGoAccount aggregates (initially ignoring snapshots).
清单 22-7 展示了存储库需要支持的三个主要操作:创建流、追加到流和加载流。创建事件流涉及创建一个带有初始事件集的新流,如Add()所示。类似地,Save()将PayAsYouGoAccount聚合中未提交的事件追加到现有流。最后,FindBy()根据其 ID加载PayAsYouGoAccount。
In Listing 22-7, the three major operations that repositories need to support are shown: creating streams, appending to streams, and loading streams. Creating an event stream involves creating a new stream with an initial set of events, as Add() shows. In a similar fashion, Save() appends the uncommitted events of a PayAsYouGoAccount aggregate to an existing stream. Lastly, FindBy() loads a PayAsYouGoAccount given its ID.
加载聚合比创建或更新稍微复杂一些,因为你必须通过指定最高和最低的事件来决定从流中加载哪些事件。最低版本号(用于将聚合重新加载到先前状态)。所有事件从事件存储中拉取后,将传入聚合的Apply() 方法,以便构建聚合的状态。这是外部服务唯一需要调用Apply() 方法的情况。
Loading aggregates is a little more complex than creating or updating because you have to decide which events should be loaded from the stream by specifying the highest and lowest version number (useful for reloading an aggregate to a previous state). Once all the events are pulled back from the Event Store, they are passed into the aggregate’s Apply() so that the aggregate’s state will be built. This is the only time when an external service should call Apply().
从清单 22-7 中需要注意的一个细节是事件流的名称,其格式为{AggregateType}-{AggregateId},这是有原因的。每个聚合一个流意味着在加载单个聚合时只需重放该聚合的事件,而不是该类型的每个聚合的事件。这具有显著的性能优势,并为平台提供了更好的可扩展性。当涉及快照时,它还可以使工作更轻松。
A detail that’s important to discern from Listing 22-7 is the name of the event stream that has the format {AggregateType}-{AggregateId}, and for good reason. Having a stream per aggregate means you only have to replay events for a single aggregate when loading it—not events for every aggregate of that type. This has significant performance advantages and provides the platform for better scalability. It can also make life easier when snapshots are involved.
如前所述,快照是一种性能增强功能,可以避免在流中加载事件的整个历史记录。快照通常由后台进程(例如 Windows 服务)创建。清单 22-8 显示了使用基于 RavenDB 的事件存储为PayAsYouGoAccount聚合创建快照的后台作业的基本示例。
Snapshots are a performance enhancement to avoid loading the entire history of events in a stream, as previously mentioned. Snapshots are usually created by a background process such as a Windows service. A basic example of a background job that creates snapshots for the PayAsYouGoAccount aggregate, using a RavenDB-based event store, is shown in Listing 22-8.
本质上,清单 22-8 中的后台作业将循环遍历所有聚合的 ID,并每 12 小时为每个聚合创建一个快照。在生产质量版本中,您可能希望遍历较小的 ID 集,可能希望添加日志记录,并且可能希望将 12 小时周期调整为更适合您环境的时间段。您还可能希望根据某些条件创建快照,例如自上次快照以来发生的事件数。
Essentially, the background job in Listing 22-8 is going to loop through the IDs of all aggregates and create a snapshot for each of them every 12 hours. In a production-quality version, you may want to iterate through smaller sets of IDs, you may want to add logging, and you will likely want to adjust the 12-hour period to something more optimal for your environment. It’s also likely that you may want to create snapshots based on some condition, such as the number of events that have occurred since the last snapshot.
生成并存储快照后,您可以使用最新快照作为初始状态来优化加载策略。清单 22-9 展示了一个利用快照的更新后的PayAsYouGoRepository.FindBy()实现。
Once you are generating and storing snapshots, you can optimize your loading strategy by using the latest snapshot as the initial state. Listing 22-9 shows an updated PayAsYouGoRepository.FindBy() implementation that takes advantage of snapshots.
您可以在清单 22-9 中看到,FindBy()已通过调用_eventStore.GetLatestSnapshot()进行更新。如果此方法返回快照,则fromEventNumber将被提升到快照之后的版本。这确保了后续对_eventStore.GetStream()的调用将仅检索该快照之后发生的事件。您还可以看到其他更改;如果找到快照,则使用快照作为初始状态创建PayAsYouGoAccount聚合。
You can see in Listing 22-9 that FindBy() has been updated with a call to _ eventStore.GetLatestSnapshot(). If this method returns a snapshot, the fromEventNumber is bumped up to the version after the snapshot. This ensures that subsequent calls to _ eventStore.GetStream() will only retrieve events that occurred after that snapshot. You can also see the other change; if a snapshot is found, the PayAsYouGoAccount aggregate is created with the snapshot as the initial state.
有时,您希望向流中添加事件的操作失败。这种情况通常发生在多个用户同时更新聚合的状态时,并且您希望避免在新的更改请求之后聚合已更新但尚未提交的情况下提交更改。您可能已经熟悉这个概念:乐观并发 ( http://technet.microsoft.com/en-us/library/aa213031(v=sql.80).aspx )。
Sometimes you want the operation of adding an event to a stream to fail. This is often the case when multiple users are updating the state of an aggregate at the same time, and you want to avoid a change being committed if the aggregate has updated after the new change was requested but not committed. You might already be familiar with this concept: optimist concurrency (http://technet.microsoft.com/en-us/library/aa213031(v=sql.80).aspx).
在事件源应用程序中,可以通过两个附加步骤实现乐观并发。首先,当聚合被加载时,它会记录事务开始时的版本号。其次,在任何新事件附加到流之前,都会进行检查,以确保最后持久化的版本号与事务开始时聚合的版本号匹配。清单 22-10 和清单 22-11 演示了这个过程。
In an event-sourced application, you can implement optimistic concurrency with two additional steps. First, when an aggregate is loaded, it keeps a record of the version number it had at the start of the transaction. Second, just before any new events are appended to the stream, a check is carried out to ensure that the last persisted version number matches the version number the aggregate had at the start of the transaction. Listing 22-10 and Listing 22-11 illustrate this process.
清单 22-10 包含一个更新的PayAsYouGoAccount聚合,它有一个InitialVersion属性,表示在聚合加载之前持久化的最后一个事件的版本。然后,该值被传递到事件存储的AppendEventsToStream()中,如清单 22-11 所示,事件存储实现可以检查以确保此版本仍然是最新的。在本章后面,您将看到一个事件存储实现,它展示了如何完整地实现此功能。
LISTING 22-10 contains an updated PayAsYouGoAccount aggregate with an InitialVersion property, representing the version of the last event that was persisted prior to the aggregate being loaded. This value is then passed into the Event Store’s AppendEventsToStream(), as shown in Listing 22-11, where the Event Store implementation can then check to ensure this version is still the most up to date. Later in the chapter, you will see an event store implementation that shows how to fully implement this feature.
事件源聚合的单元测试涉及断言事件是否已发生。这是通过检查属于聚合的未提交事件的集合来完成的。清单 22-12 展示了一个针对PayAsYouGoAccount.TopUp()的示例。
Unit testing event-sourced aggregates involves asserting that events have occurred. This is done by checking the collection of uncommitted events belonging to the aggregate. An example demonstrating this for PayAsYouGoAccount.TopUp() is shown in Listing 22-12.
在PayAsYouGoAccount应用了包含分钟数优惠后,如果客户充值达到包含分钟数优惠的阈值,则除了充值金额外,还会获得免费积分。清单 22-12 中的测试验证了当充值不符合条件时,不会应用免费分钟数。它通过断言聚合的未提交事件中只存在一个CreditAdded事件,并且聚合的状态仅增加了充值金额来实现这一点。您可以通过清单 22-12 底部对Assert.Equal()的调用来看到这一点。
After a PayAsYouGoAccount has an inclusive minutes offer applied to it, if the customer tops up by the inclusive minute offer’s threshold, the customer is given free credits in addition to her top-up amount. The test in Listing 22-12 verifies that when a top up does not meet the criteria, no free minutes are applied. It does this by asserting that only a single CreditAdded event exists within the aggregate’s uncommitted events, and the state of the aggregate has been increased only by the amount of the top up. You can see this with the calls to Assert.Equal() at the bottom of Listing 22-12.
清单 22-12 是测试事件源聚合的常见情况;您将通过富有表现力的高级应用程序编程接口 (API) 与它们交互,例如TopUp()。然后,您将断言事件已添加到未提交列表中,并且状态会相应更改。这通常适用于高级集成测试和低级单元测试。主要区别在于您是模拟协作者和外部服务,还是传入真实的实现。
LISTING 22-12 is a common case when testing event-sourced aggregates; you will interact with them through expressive high-level application programming interfaces (APIs), like TopUp(). You will then assert that events were added to the uncommitted list, and state changed accordingly. This will often apply to both high-level integration tests and low-level unit tests. The major difference is whether you mock collaborators and external services or pass in real implementations.
要构建使用事件溯源的应用程序,您可以使用专门构建的事件存储,也可以创建自己的事件存储。创建自己的事件存储实际上意味着以新颖的方式使用现有工具。因此,在本节中,您将了解如何使用 RavenDB 和 SQL Server 作为事件溯源的基础。商店。创建自己的活动商店也是增加对核心概念理解的一种实践方法。
To build applications that use event sourcing, you can use a purpose-built event store or create one of your own. Creating one of your own really just means using an existing tool in a novel way. So in this section, you will see how you can use RavenDB and SQL Server as the basis for event stores. Creating your own event store is also a hands-on way to increase your understanding of the core concepts.
您之前在清单 22-11 中看到了PayAsYouGoAccoutRepository使用的IEventStore接口。这是本节中创建的事件存储将实现的接口。清单 22-13 完整显示了IEventStore 接口。
You saw the IEventStore interface being used previously by the PayAsYouGoAccoutRepository in Listing 22-11. This is the interface that the Event Store being created in this section will implement. IEventStore is shown in its entirety in Listing 22-13.
设计或选择存储格式是构建事件存储功能时面临的一大挑战。使用您选择的技术,您至少需要提供创建事件流、向其中附加事件以及按相同顺序提取这些事件的功能。您还可能需要支持快照功能。
Designing or choosing a storage format is one of the big challenges when building event store functionality. Using your chosen technology, at a minimum you need to provide the ability to create event streams, append events to them, and pull those events back out again in the same order. It’s also likely that you will want to support snapshotting.
在此示例中,存储格式基于三种文档类型:EventStream、EventWrapper和SnapshotWrapper。EventStream表示一个事件流,但仅包含其 ID 和 Version 等元数据。EventWrapper包装并表示一个单独的域事件,包含该事件的所有数据以及其所属流的元数据。SnapshotWrapper表示单个快照。另一种策略是使用单个文档,其中包含每个聚合的元数据、事件和快照。您可以自由地设计和完善您自己的格式。
In this example, the storage format is based on three document types: EventStream, EventWrapper, and SnapshotWrapper. EventStream represents an event stream but only contains meta data such as its Id and Version. EventWrapper wraps and represents an individual domain event, containing all the event’s data plus meta data about the stream it belongs to. SnapshotWrapper represents a single snapshot. An alternative strategy would have been to have a single document that contained the meta data, events, and snapshots for each aggregate. You are free to design and refine your own format as you see fit.
RavenDB 是一个文档数据库,其中每个文档都是一个 JavaScript 对象表示法 (JSON) 对象。Raven 的 C# 客户端库会自动将普通旧式 C# 对象 (POCO) 类转换为所需的 JSON。这意味着,用作事件存储功能基础的三种文档类型只需在代码中声明即可。EventStream 、 EventWrapper和SnapshotWrapper分别如清单 22-14 至 22-16 所示。
RavenDB is a document database in which each document is a JavaScript Object Notation (JSON) object. Handily, Raven’s C# client library automatically converts Plain Old C# Object (POCO) classes into the required JSON for you. This means that the three document types to be used as the basis for event store functionality need only be declared in code. EventStream, EventWrapper, and SnapshotWrapper are shown in Listings 22-14 through 22-16, respectively.
现在您已经选择了一种文档类型来表示事件流,创建事件流的实例涉及创建该类型的文档。这将通过支持创建流的事件存储的初始实现来演示,如清单22-17所示。
Now that you’ve chosen a document type to represent an event stream, creating an instance of an event stream involves creating a document of that type. This is demonstrated with the initial implementation of an event store that supports the creation of streams, as shown in Listing 22-17.
RavenDB 是其IDocumentSession接口所代表的底层存储技术。您可以看到清单 22-17 中的CreateNewStream()实现使用IDocumentSession来存储EventStream对象。如上所述,RavenDB 将此EventStream对象转换为 JSON 并为其创建一个新文档。从概念上讲,在 RavenDB 创建EventStream文档时,新的事件流已经创建。但是,它不包含任何事件。
RavenDB is the underlying storage technology represented by its IDocumentSession interface. You can see the implementation of CreateNewStream() in Listing 22-17 is using an IDocumentSession to store an EventStream object. As mentioned, RavenDB converts this EventStream object to JSON and creates a new document for it. Conceptually, at the point RavenDB creates the EventStream document, the new event stream has been created. However, it doesn’t contain any events.
一旦创建了事件流,下一个挑战就是提供向其中附加事件的功能。在本例中,一个事件由EventWrapper表示,该类用元数据包装了领域事件,以便在持久化时能够将其与正确的事件流关联起来。清单 22-18 显示了支持附加EventWrapper的EventStore的更新实现。
Once event streams have been created, the next challenge is to provide the ability to append events to them. In this example, an event is represented by an EventWrapper, a class that wraps a domain event with meta data so that when it is persisted, it can be associated with the correct event stream. An updated implementation of EventStore that supports appending EventWrappers is shown in Listing 22-18.
AppendEventsToStream()将每个EventWrapper存储为一个唯一的文档。但这是一个实现细节;所有AppendEventsToStream()的调用者都知道事件被附加到相应的流中。但是,由于每个事件都是一个唯一的文档,因此需要某种方法来检索给定流的所有事件。EventWrapper包含实现此功能的元数据:流的 ID 和事件版本号。这些值由 EventStream.RegisterEvent() 填充,如清单 22-14 所示。正如您将在下一个示例中看到的,查询流中的事件主要就是搜索EventStreamId与正在查询事件的流的名称匹配的事件。
AppendEventsToStream() stores each EventWrapper as a unique document. But this is an implementation detail; all callers of AppendEventsToStream() know is that the event was appended to the appropriate stream. However, because each event is a unique document, there needs to be some way of retrieving all events for a given stream. EventWrapper contains the meta data that enables this: the ID of the stream and an event version number. These values are populated by EventStream.RegisterEvent(), as shown previously in Listing 22-14. As you’ll see in the next example, querying for events in a stream is then mainly a case of searching for events whose EventStreamId matches the name of the stream whose events are being queried.
构建事件存储功能时,至少需要包含查询流中所有事件的功能。您还需要提供按事件版本或 ID 查询的功能,以支持快照加载。清单 22-19 显示了提供这些功能的EventStore的更新实现。
When building event store functionality, as a minimum you need to include the ability to query for all the events in a stream. You will also need to provide the ability to query by the version or ID of an event to support snapshot loading. An updated implementation of EventStore that provides these capabilities is shown in Listing 22-19.
如前所述,本例中表示事件流的约定是将每个事件存储为一个唯一的EventWrapper文档,其中包含其所属流的 ID。在清单 22-19 中可以看到,where子句遵循此约定,指定只应选择具有传入流名称的EventWrapper 。
As previously discussed, the convention for representing event streams in this example is to store each event as a unique EventWrapper document containing the ID of the stream it belongs to. You can see in Listing 22-19 that the where clause adheres to this convention by specifying that only EventWrappers with the passed-in stream name should be selected.
除了加载流的所有事件外,fromVersion和toVersion参数也用作查询的一部分。它们分别指定应从流中拉取的最早事件和最新事件。正如您将在下一个示例中看到的那样,当涉及快照时,这是一项非常重要的功能。
In addition to loading all events for a stream, the fromVersion and toVersion parameters are used as part of the query. They specify the earliest and latest event that should be pulled back from the stream, respectively. As you will see in the next example, this is an important capability when snapshots are involved.
要启用快照,必须提供存储快照的功能。在本例中,SnapshotWrapper用于将快照与附加元数据(所属流的名称和添加日期)一起包装。清单 22-20 演示了EventStore的更新版本,该版本具有保存快照的功能。
To enable snapshots, you must provide the facility to store them. In this example, SnapshotWrapper is used to wrap snapshots with additional meta data—the name of the stream it belongs to and the date it was added. Listing 22-20 demonstrates an updated version of EventStore with the ability to save snapshots.
AddSnapshot<T>()允许消费者存储给定流的快照,方法是使用SnapshotWrapper包装快照,其中包含快照所属流的名称和保存时间。如清单 22-21 所示,这两个元数据允许EventStore的消费者只需传入流名称即可获取任何流的最新快照。
AddSnapshot<T>() allows consumers to store a snapshot for a given stream by wrapping the snapshot with a SnapshotWrapper that contains the name of the stream it belongs to and the time it was saved. As Listing 22-21 shows, these two pieces of meta data allow consumers of EventStore to get the latest snapshot for any stream simply by passing in the name of the stream.
希望您能通过观察清单 22-21 中的代码看到使用SnapshotWrapper的好处。它的StreamName和Created字段用于构建一个查询,该查询根据传入的流的名称查找最新保存的快照。与AddSnapshot<T>()结合使用,这是一个简洁的 API,用于存储任何类型的快照并在稍后将其提取出来。但这只是一种可能的解决方案;您可能会找到更适合您需求的替代解决方案。鼓励创造性思维。
Hopefully, you can see the benefits of using SnapshotWrapper by observing the code in Listing 22-21. Its StreamName and Created fields are used to build a query that finds the latest saved snapshot for the name of the passed-in stream. In combination with AddSnapshot<T>(), this is a clean API for storing a snapshot of any type and pulling it out later. But this is just one possible solution; you may find alternative solutions that fit your needs better. Creative thinking is encouraged.
许多现代应用程序都是多用户的,这意味着不同的用户会同时查看和更新同一条数据。如果一个用户尝试更新某些数据,而另一个用户在不知情的情况下更改了该数据,那么更新有时必然会失败。如上所述,这就是乐观并发。在事件存储中处理乐观并发的一种通用方法是验证上次存储事件的版本号是否与新更新所基于的版本号匹配。
Many modern applications are multiuser, meaning that different users view and update the same piece of data at the same time. If one user attempts to update some data but another has changed it without the other user realizing, it’s sometimes essential that the update fails. As mentioned, this is optimistic concurrency. A generic way of handling optimistic concurrency in event stores is to verify that the version number of the last stored event matches the version number that the new update is based on.
清单 22-11 展示了如何在应用程序级别跟踪预期版本,其中InitialVersion属性被添加到聚合中,然后在每次将新事件附加到流时传递到事件存储中。清单 22-22 展示了EventStore.AppendEventsToStream()的更新版本如何使用此版本号在预期版本号与上次存储事件的版本号不匹配时中止保存任何事件——这意味着自此事务开始以来已添加了新事件,因此用户可能没有意识到这一点。
Keeping track of the expected version at the application level was shown previously in Listing 22-11, where the InitialVersion property was added to the aggregate and then passed into the event store each time new events were appended to the stream. Listing 22-22 shows how an updated version of EventStore.AppendEventsToStream() uses this version number to abort saving any events if the expected version number does not match the version number of the last stored event—meaning a new event(s) has been added since this transaction began, and thus the user was probably not aware of it.
如果将预期的版本号传入AppendEventsToStream,EventStore会确保在附加新事件之前,传入的版本号与上次存储的事件的版本号匹配。如清单 22-22 所示。具体来说,私有CheckForConcurrencyError()执行比较逻辑,如果版本号不同,则抛出OptimisticConcurrencyException异常。
If an expected version number is passed into AppendEventsToStream, the EventStore ensures that before appending new events, the passed-in version number matches the version number of the last stored event. This is shown in Listing 22-22. In particular, the private CheckForConcurrencyError() carries out the comparison logic and throws an OptimisticConcurrencyException if the version numbers aren’t the same.
不幸的是,清单 22-22 中的解决方案在与 RavenDB 一起使用时不够健壮,因为可能存在竞争条件。如果仔细观察,您会注意到在CheckForConcurrencyError()和documentSession.Store()完成之间会有一小段时间。如果在此期间将新事件附加到流中,会发生什么?这两个事件都会被附加到流中,即使CheckForConcurrenyError()旨在防止这种情况发生。幸运的是,您可以将 RavenDB 配置为乐观并发,因此如果自事件发生以来流已更新,它就不会存储事件。
Unfortunately, the solution in Listing 22-22 is not robust enough when used with RavenDB because of the possibility of a race condition. If you look carefully, you’ll notice there will be a short period between the completion of CheckForConcurrencyError() and _ documentSession.Store(). What would happen if a new event was appended to the stream in this period? Both events would be appended to the stream, even though CheckForConcurrenyError() aims to prevent this. Fortunately, you can configure RavenDB for optimistic concurrency, so it does not store an event if the stream has been updated since the event occurred.
无论使用哪种技术构建事件存储,如果支持乐观并发,竞争条件很可能是您需要处理的一个因素。锁定流并检查最后提交事件的 ID 很可能是您为此所依赖的过程。要锁定存储在 RavenDB 中的事件流,第一步是启用乐观并发,如清单 22-23 所示。
Whichever technologies you use to build an event store, race conditions will likely be a factor you need to deal with if you’re supporting optimistic concurrency. Locking the stream and checking the ID of the last committed event is likely to be the process you will rely on for this. To lock an event stream stored in RavenDB, the first step is to enable optimistic concurrency, as shown in Listing 22-23.
RavenDB 支持乐观并发。清单 22-23 展示了如何通过将session.Advanced.UseOptimisticConcurreny设置为true来启用它。清单 22-23 还展示了,这可以在服务层进行配置(更多解释见第 25 章:“命令:用于处理业务用例的应用服务模式”),其中对象的生命周期通常由容器管理。(ObjectFactory是清单 22-23 中的 IoC 容器。)本质上,清单 22-23 中的代码确保每个新的 Web 请求或每个线程在启用乐观并发的情况下获得自己的文档会话。每个线程或 Web 请求获得自己的会话非常重要,因为会话中所做的所有更改都可以一起回滚。
RavenDB provides support for optimistic concurrency. Listing 22-23 shows how it is enabled by setting session.Advanced.UseOptimisticConcurreny to true. As Listing 22-23 also shows, this can be configured in the service layer (explained more in Chapter 25: “Commands: Application Service Patterns for Processing Business Use Cases”), where the lifecycle of objects is managed, often with a container. (ObjectFactory is the IoC container in Listing 22-23.) Essentially, the code in Listing 22-23 ensures that each new web request, or each thread, gets its own document session with optimistic concurrency enabled. It’s important for each thread or web request to get its own session because all changes made in a session can be rolled back together.
一旦启用,RavenDB 就会强制执行乐观并发,方法是中止事务,并在当前IDocumentSession创建后,如果存储的事件在另一个IDocumentSession中被修改,则抛出ConcurrencyExceptions 。清单 22-24 通过展示由TopUpCredit应用服务执行的完整业务用例,说明了如何将其应用于EventStore。
Once enabled, RavenDB enforces optimistic concurrency by aborting transactions and throwing ConcurrencyExceptions when an event being stored has been modified in another IDocumentSession since the current IDocumentSession was created. Listing 22-24 illustrates how this applies to EventStore by showing a full business use case that is carried out by the TopUpCredit application service.
IDocumentSession的实例在 Web 请求开始时创建,并传递到清单 22-24 所示的TopUpCredit应用服务中。然后, TopUpCredit使用存储库查找相关帐户并充值其信用额度,从而将域事件添加到聚合的未提交事件列表中。当在存储库上调用Save()时,这些未提交的事件将排队存储在 RavenDB 中。最后,当在IDocumentSession( _ unitOfWork )上调用SaveChanges()时,RavenDB 提交更改并将其持久化到磁盘。但是,如果 RavenDB 注意到自传递到TopUpCredit的IDocumentSession启动以来,这些事件所附加到的事件流已被另一个IDocumentSession修改,则它不会提交更改。相反,它会抛出ConcurrencyException,您可以在清单 22-24 中看到它是如何处理的。
An instance of IDocumentSession is created at the start of a web request and passed into the TopUpCredit application service shown in Listing 22-24. TopUpCredit then finds the relevant account using a repository and tops up its credit, causing domain events to be added to the aggregate’s list of uncommitted events. These uncommitted events are queued up for storage in RavenDB when Save() is called on the repository. Finally, when SaveChanges() is called on the IDocumentSession ( _ unitOfWork), RavenDB commits the changes and persists them to disk. However, if RavenDB notices that the event stream these events are being appended to has been modified by another IDocumentSession since the IDocumentSession passed into TopUpCredit was started, it does not commit the changes. Instead, it throws the ConcurrencyException you can see being handled in Listing 22-24.
SQL Server 是构建事件存储的另一种常见存储选项。目前有多个开源项目提供指导和参考实现,以帮助您完成这项工作。Ncqrs(https://github.com/ncqrs/ncqrs/blob/master/Framework/src/Ncqrs/Eventing/Storage/SQL/MsSqlServerEventStore.cs)和 NEventStore(https://github.com/NEventStore/NEventStore/blob/master/src/NEventStore/Persistence/Sql/SqlPersistenceEngine.cs)是其中最受欢迎的。以下简短章节将以 Ncqrs 为例进行分析。
SQL Server is another common storage option for building event stores. There are several open source projects that provide guidance and reference implementations to help you along. Ncqrs (https://github.com/ncqrs/ncqrs/blob/master/Framework/src/Ncqrs/Eventing/Storage/SQL/MsSqlServerEventStore.cs) and NEventStore (https://github.com/NEventStore/NEventStore/blob/master/src/NEventStore/Persistence/Sql/SqlPersistenceEngine.cs) are among the most popular. Ncqrs is used as a case study in the following short section.
当你选择使用 SQL 来存储事件和流时,一个重要的考虑因素是模式。那些在 SQL 之上构建事件存储的人建议,在将事件数据存储为 XML 或 JSON 格式时,尽量简化模式。。建议是一个通用的、与领域无关的模式,如清单 22-25 所示。
When you’re choosing to use SQL to store events and streams, an important consideration is schema. Those who have built event stores on top of SQL suggest being minimalistic with schema while storing event data as blobs of XML or JSON. The recommendation is a generic, domain-agnostic schema like the one shown in Listing 22-25.
与您之前看到的 RavenDB 事件存储的一个区别是,此模式使用[uniqueidentifiers](也称为GUID)作为事件流(在清单 22-25 中称为事件源)的 ID。在您的应用程序中,您可以选择任何您喜欢的类型。除了这个区别之外,您还可以看到与基于 RavenDB 的方法的相似之处,其中每个事件都是一个唯一的记录,并且流的概念是逻辑概念,它依赖于通过事件所属流的 ID 来关联事件。
One difference to the RavenDB event store you previously saw is that this schema uses [uniqueidentifiers], also known as GUIDs, as the ID for an event stream (referred to in Listing 22-25 as an event source). In your application, you can get away with whichever type you prefer. Apart from that difference, you can see a similarity to the RavenDB-based approach, where each event is a unique record, and the concept of a stream is a logical one that relies on associating events by the ID of the stream they belong to.
Ncqrs 事件存储的大部分实现都通过模式来避免。例如,创建一个新流只需向EventSources表添加一条新记录即可。清单 22-26 对此进行了示例。请注意,Ncqrs 使用了术语“事件源” ,在本章的其他地方也提到过“事件流” 。
Most of the implementation of the Ncqrs event store is obviated by the schema. For example, creating a new stream is simply a case of adding a new record to the EventSources table. This is exemplified in Listing 22-26. Do note that Ncqrs uses the term event source, which is what has been referred to as an event stream elsewhere in the chapter.
该模式还避免了保存事件。清单 22-27 通过展示如何将每个事件作为新行添加到 Events 表中来演示这一点,并引用它所属的事件流。
The schema also obviates saving events. Listing 22-27 demonstrates this by showing how each event is added as a new row in the Events table, with a reference to the event stream it belongs to.
清单 22-28 使用了 Ncqrs 的一个示例,该示例从流中读取事件。这类似于本章前面展示的IEventStore.GetStream(),并且具有类似的逻辑。如果所有引用传入流名称 ( id ) 的事件都在提供的minVersion和maxVersion范围内,则返回这些事件。
Listing 22-28 uses a sample from Ncqrs that reads events from a stream. This is akin to IEventStore.GetStream() shown earlier in the chapter, and with similar logic. All events that reference the passed-in stream name (id) are returned if they fall within the range of the supplied minVersion and maxVersion.
您可以在 GitHub 上看到清单 22-28 中引用的ReadEventFromDbReader()的完整实现( https://github.com/ncqrs/ncqrs/blob/master/Framework/src/Ncqrs/Eventing/Storage/SQL/MsSqlServerEventStore.cs)。
You can see the full implementation of ReadEventFromDbReader() referenced in Listing 22-28 on GitHub (https://github.com/ncqrs/ncqrs/blob/master/Framework/src/Ncqrs/Eventing/Storage/SQL/MsSqlServerEventStore.cs).
Ncqrs 中使用专用表来存储快照。清单 22-29 使用事件流(也称为事件源)的 ID 展示了如何保存快照,清单 22-30 展示了如何加载快照。
A dedicated table is used to store snapshots in Ncqrs. Using the ID of event stream (also known as an event source), Listing 22-29 shows how snapshots are saved, and Listing 22-30 shows how they are loaded.
在事件溯源的早期,没有商业工具。相反,开发人员必须在现有技术(例如 SQL 数据库或文档数据库)之上构建事件溯源功能,如本节所示。这意味着它绝对是可以实现的。但是,如果您转向更高级的场景,例如投影、复杂的时态查询和增强的可扩展性,您可能会发现花费了大量时间却无法增加业务价值。因此,您可能需要考虑一种专门构建的技术,例如 Greg Young 的 Event Store(也称为 Event Store),它提供了许多开箱即用的高级功能。
During the early days of event sourcing, there were no commercial tools. Instead, developers had to build event-sourcing capabilities on top of existing technologies like SQL databases or document databases, as shown in this section. This means that it’s definitely achievable. But if you move on to more advanced scenarios like projections, complex temporal queries, and enhancing scalability, you may find you are spending a lot of time away from adding business value. This is why you may want to consider a purpose-built technology like Greg Young’s Event Store (also known as the Event Store) that provides many advanced features out of the box.
选择使用现有的事件存储可以减少您开始事件溯源所需的前期工作量。此外,通过选择像 Greg Young 的 Event Store 这样的事件存储,您可以获得许多开箱即用的附加功能,包括高级投影和用于高度可扩展环境的多节点集群。为了演示如何使用事件存储,本节中的示例仍然围绕虚构的手机网络运营商提供的即用即付服务。您将了解如何使用 Event Store C# 客户端库构建替代的IEventStore实现,以及如何在管理 Web UI 中运行查询和投影。要完成这些示例,您需要在计算机上安装 Event Store。
Choosing to use an existing event store reduces the amount of work you have to do up front to get started with event sourcing. Also, by choosing an event store like Greg Young’s Event Store, you get many additional features out of the box, including advanced projections and multinode clustering for highly scalable environments. To demonstrate using event store, the examples in this section remain oriented around the pay-as-you-go service offered by a fictitious cell phone network operator. You see how to build an alternative IEventStore implementation using the Event Store C# client library, as well as running queries and projections in the admin web UI. To work through these examples, you need Event Store installed on your machine.
要运行投影,需要 Event Store 版本 3.0.0 rc2 ( http://download.geteventstore.com/binaries/eventstore-net-v3.0.0rc2.zip )。下载 zip 文件后,您需要将其解压到您选择的文件夹中。在解压到的目录中,您可以使用以下 PowerShell 命令启动 Event Store。您需要以管理员权限启动 PowerShell。
To run projections, Event Store version 3.0.0 rc2 is required (http://download.geteventstore.com/binaries/eventstore-net-v3.0.0rc2.zip). Once you’ve downloaded the zip file, you need to extract it to a folder of your choice. From within the directory you extract to, you can start Event Store with the following PowerShell command. You need to start PowerShell with Administrator privileges.
.\EventStore.SingleNode.exe ––db .\ESData ––run-projections=all
.\EventStore.SingleNode.exe ––db .\ESData ––run-projections=all
要启用投影功能,需要进行一些调整。导航到管理 Web UI ( http://localhost:2113/projections ),您可以通过访问“投影”选项卡并启动以下投影来启用投影:$by_category和$stream_by_category。(单击链接,然后单击“开始”按钮。)
A few tweaks are necessary to enable projections. By navigating to the admin web UI (http://localhost:2113/projections), you can enable projections by accessing the Projections tab and starting the following projections: $by _ category and $stream _ by _ category. (Click on the link and then click the Start button.)
您可以通过编写使用官方 Event Store C# 客户端库的持久层,在应用程序中充分利用 Event Store。或者,Event Store 也提供超文本传输协议 (HTTP) API,您将在第 26 章“查询:域报告”中看到。在本例中,您将看到IEventStore的新实现,它可以与您在本章前面创建的 RavenDB 版本切换。此实现依赖于以下 NuGet 包:
You can take advantage of Event Store in your applications by writing a persistence layer that uses the official Event Store C# client library. Alternatively, Event Store has a Hypertext Transport Protocol (HTTP) API that you will see in Chapter 26: “Queries: Domain Reporting.” In this example, you see a new implementation of IEventStore that can be switched with the RavenDB version you created earlier in the chapter. This implementation relies on the following NuGet packages:
Install-Package EventStore.Client -version 2.0.2.0
Install-Package Newtonsoft.Json -version 6.0.3
Install-Package EventStore.Client -version 2.0.2.0
Install-Package Newtonsoft.Json -version 6.0.3
EventStore.Client是 Event Store 团队创建的官方客户端库。它使用 TCP 与 Event Store 通信。由于事件以 JSON 格式存储,因此需要使用Newtonsoft.Json;因此,它们需要与 C# 类进行序列化。清单 22-31 展示了IEventStore的GetEventStore实现,它提供了这些细节的具体示例。
EventStore.Client is the official client library, created by the Event Store team. It uses TCP to communicate with Event Store. Newtonsoft.Json is necessary because events are stored as JSON; therefore, they need to be serialized to and from C# classes. Listing 22-31 shows the GetEventStore implementation of IEventStore that provides concrete examples of these details.
值得花一些时间看一下清单 22-31。请注意,使用专用事件存储比本章前面展示的基于 RavenDB 或 SQL Server 的方法需要更少的工作。两个值得注意的例子是查询事件和乐观并发支持。在GetEventStream()中查询事件时,只需传入流的名称、起始版本和要检索的事件数。在 RavenDB 解决方案中,有一个包含多个子句的中等复杂查询。这是因为事件存储本身支持流的概念,而不是逻辑上模拟流。
It’s worth spending a few moments looking at Listing 22-31. Notice how using a purpose-built event store requires less work than the RavenDB or SQL Server-based approaches shown earlier in the chapter. Two noticeable examples of this are querying for events and optimistic concurrency support. When querying for events in GetEventStream(), you simply pass in the name of the stream with a starting version and number of events to retrieve. In the RavenDB solution, there was a moderately complex query with a number of clauses. This is because Event Store natively supports the concept of a stream, rather than logically simulating one.
原生流支持也是乐观并发实现工作量较少的原因。正如您在 Event Store 的IEventStoreConnection.AppendToStream()中看到的那样,您只需传入最后一个存储事件的预期版本号;Event Store 会为您处理所有乐观并发检查和管理。
Native stream support is also the reason optimistic concurrency is less work to implement. As you can see with Event Store’s IEventStoreConnection.AppendToStream(), you only have to pass in the expected version number of the last stored event; Event Store takes care of all the optimistic concurrency checking and management for you.
RavenDB 的一个优势是它能够处理 JSON 序列化和反序列化。在清单 22-31 中,您可以看到事件的类型(C# 类的名称)作为标头存储在MapToEventStoreStorageFormat()中。然后,当从 JSON 重建事件时,将在返回过程中使用此标头,如RebuildEvent()所示。在这两种情况下,都需要您自己协调序列化和反序列化逻辑。
One advantage that RavenDB does have is that it deals with the serialization to JSON and back. In Listing 22-31, you can see that the type of the event—the name of the C# class—is stored as a header in MapToEventStoreStorageFormat(). This header is then used on the way back out when events are reconstructed from JSON, as shown in RebuildEvent(). In both cases, there is a need to coordinate the serialization and deserialization logic yourself.
快照支持是通过为每个聚合的快照创建新的事件流来实现的。使用专门构建的事件存储也非常适合此用例。您可以在GetLatestSnapshot()中看到,只需依靠IEventStore.ReadStreamEventsBackwards()并传入 1 作为要返回的事件数量,即可进行一次查询来检索最新快照。
Snapshot support is implemented by creating a new event stream for each aggregate’s snapshots. Using a purpose-built Event Store shines for this use case as well. You can see in GetLatestSnapshot() that you can make a single query to retrieve the latest snapshot by really leaning on IEventStore.ReadStreamEventsBackwards(), passing in 1 as the number of events to return.
一个未显示的重要细节是创建与 Event Store 的初始连接。清单 22-32 展示了如何使用客户端库的EventStoreConnection.Create()来执行此操作。
One important detail that isn’t shown is creating the initial connection to Event Store. Listing 22-32 shows how you can use the client library’s EventStoreConnection.Create() to do this.
清单 22-32 显示了如何创建与在默认端口 1113 上本地运行的事件存储实例的 TCP 连接。但是,您应该将端口和地址更改为您配置事件存储运行时的端口和地址。
LISTING 22-32 shows how to create a TCP connection to an Event Store instance running locally on the default port of 1113. However, you should change the port and address to those that you configured Event Store to run on.
Greg Young 和 Event Store 团队已经确定 JavaScript 是进行查询和创建投影的最佳工具。很快,您将在 Event Store 的用户友好型 Web UI 中看到创建时间查询的示例。不过,首先,您需要将一些数据导入到本地运行的 Event Store 版本中。本章的示例代码包含EventStoreDemo项目中一个名为ImportTestData的类。它有一个可以在 Visual Studio 中运行的TestMethod ,它会使用PayAsYouGoAccount聚合集合的大量事件填充 Event Store 。运行测试后,您可以通过导航到 Web UI 中的“流”选项卡(http://localhost:2113/web/streams.htm )来查看新创建的流及其事件。您应该看到类似于图 22.4的活动,表明流已创建。
Greg Young and the Event Store team have decided that JavaScript is the best tool for making queries and creating projections. Shortly, you will see examples of creating temporal queries in Event Store’s user-friendly web UI. First, though, you need to import some data into your locally running version of Event Store. This chapter’s sample code contains a class called ImportTestData in the EventStoreDemo project. This has a TestMethod that you can run from within Visual Studio, and it populates Event Store with a number of events for a collection of PayAsYouGoAccount aggregates. Once you have run the test, you can see the newly created streams and their events by navigating to the Stream tab in the web UI (http://localhost:2113/web/streams.htm). You should see activity similar to Figure 22.4, indicating that the streams were created.
图 22.4事件存储的流选项卡指示测试数据已成功插入。
FIGURE 22.4 Event Store’s stream tab indicating the test data was successfully inserted.
尽管听起来有点花哨,但时间查询不必庞大而复杂。一个可能有用的简短查询是向客户显示他在某一天使用了多少分钟。可以是当天,也可以是过去的某个日期。这有助于客户了解他们是否过度使用手机。清单 22-33 展示了此查询所需的 JavaScript。
Despite sounding a bit fancy, temporal queries don’t have to be big and complicated. A short query that might be useful is showing a customer how many minutes he used on a given day. It could be the current day, or it could be a date in the past. This helps customers understand if they are using their phone too much. Listing 22-33 shows the JavaScript needed for this query.
当您指定要查询的事件时,事件存储中的查询就开始了。在清单 22-33 中,fromStream()指定查询应应用于代表单个PayAsYouGoAccount的单个流中的所有事件。您可以从 Web UI 中的 Streams 选项卡中获取事件流的 ID。流中的每个事件(从最旧的事件开始)随后被传递到when()中。when ()内部是一个模式匹配,用于确定处理哪些事件以及如何处理。在清单 22-33 中,您可以看到只处理PhoneCallCharged类型的事件。
Queries in Event Store begin when you specify which events you want to query. In Listing 22-33, fromStream() is specifying that the query should apply to all events in a single stream representing a single PayAsYouGoAccount. You can get the ID of an event stream from the Streams tab in the web UI. Each event in the stream, starting with the oldest, is then passed into when(). Inside when() is a pattern match that determines which events are handled and how. In Listing 22-33, you can see that only events of type PhoneCallCharged are handled.
事件存储查询的结果是一个称为查询状态的 JavaScript 对象。此对象随每个事件传递到when()中,并根据查询逻辑重复更新。您可以看到,清单 22-33 中的状态在$init处理程序中初始化为一个 JavaScript 对象,该对象具有一个名为minutes的属性。您可以自行决定每个查询中此状态的结构。它必须是有效的 JavaScript。清单 22-33 通过在每次处理代表 6 月 4 日电话呼叫的PhoneCallCharged事件时扩充其minutes属性来使用该状态。
The result of an Event Store query is a JavaScript object known as the state of the query. This object is passed into when() with each event and is updated repeatedly as per your query logic. You can see that the state in Listing 22-33 is initialized inside the $init handler as a JavaScript object with a single property called minutes. It’s up to you to decide the structure of this state in each of your queries. It just has to be valid JavaScript. Listing 22-33 uses the state by augmenting its minutes property each time a PhoneCallCharged event, which represents a phone call on June 4, is handled.
在 Event Store 的 Web UI( http://localhost:2113/web/query.htm )的【查询】选项卡上运行清单 22-33 中的查询后,您将看到类似于图 22.5的输出。
After running the query in Listing 22-33 on the Query tab of Event Store’s web UI (http://localhost:2113/web/query.htm), you will see output similar to Figure 22.5.
图 22.5在事件存储 Web UI 中运行查询的结果。
FIGURE 22.5 Result of running a query in the Event Store web UI.
图 22.5显示了事件存储如何在右侧的“状态”窗格中将查询结果显示为其最终状态。如上所述,您可以自行确定状态的结构,并可以拥有任何必要的属性。例如,您可能希望增强清单 22-33 中的查询,以保留多个日期使用的分钟数的总数,其中每个日期都是状态的单独属性。此查询如清单 22-34 所示。
Figure 22.5 shows how Event Store displays the results of a query as its final state in the right State pane. As mentioned, you determine the structure of the state yourself and can have whatever properties are necessary. For example, you might want to enhance the query in Listing 22-33 to keep a total for the number of minutes used on multiple dates, where each date is a separate property on the state. This query is shown in Listing 22-34.
在清单 22-34 中,状态包含三个属性,分别对应每个日期。每个属性都会增加相应日期通话所用的时间。
In Listing 22-34, the state contains three properties, corresponding to each date. Each of these properties is increased by the number of minutes used up during phone calls on the relevant date.
事件存储并不局限于查询单个流。对于许多用例来说,组合来自多个流的数据非常重要,类似于 SQL 中的连接。清单 22-35 展示了如何计算所有即用即付账户在每个日期使用的总分钟数。
Event Store is not limited to querying a single stream. For many use cases, it’s important to combine data from multiple streams, akin to joins in SQL. Listing 22-35 shows how the combined total minutes used on each date can be calculated for all pay-as-you-go accounts.
Event Store 的fromCategory()方法的工作方式是组合所有以传入名称开头并以连字符结尾的流。例如,清单 22-35 中的查询匹配所有以PayAsYouGoAccount-开头的流中的所有事件。然而,有一个小问题是 Event Store 会查找最后一个连字符。这就是清单 22-35 中GetEventStore类中private StreamName()方法的原因,该方法删除了除第一个连字符之外的所有连字符。
Event Store’s fromCategory() works by combining all streams whose name begins with the passed-in name, suffixed with a hyphen. For example, the query in Listing 22-35 matches all events in all streams whose name begins with PayAsYouGoAccount-. However, a small gotcha is that Event Store looks for the last hyphen. This is the reason for the private StreamName() method in the GetEventStore class shown in Listing 22-35, which removes all but the first hyphen.
除了计算状态之外,有时您还希望能够获取事件的子集并从中创建一个全新的流。这对于具有数百万或数十亿个事件的大型系统至关重要。以手机网络运营商为例;如果有数百万个客户,每个客户都有大量账户活动,那么很容易就会有数十亿个事件。对所有这些事件运行查询效率低下。投影通过让您只选择您感兴趣的事件来解决这个问题,方法是将它们放入新流中,然后针对新创建的流运行查询。清单 22-36 显示了一个投影,它将所有客户的所有充值分组到一个名为AllTopUps的新流中。
Instead of just calculating state, sometimes you want the ability to take subsets of events and create an entirely new stream from them. This can be crucial in a large system with millions or billions of events. Take the cell phone network operator, for example; if there a millions of customers each with lots of account activity, there could easily be billions of events. It would be inefficient to run queries across all of them. Projections solve this problem by letting you select only events that you are interested in by putting them in a new stream and then running your query against the newly created stream. Listing 22-36 shows a projection that groups all the top ups made by all customers into a new stream called AllTopUps.
运行清单 22-36 中的投影会导致任何PayAsYouGoAccount流中存在的所有CreditAdded事件被添加到名为AllTopUps的新流中。这意味着可以针对所有CreditAdded事件运行查询,而不必加载流中的所有其他事件,例如PhoneCallCharged。Event Store 提供此行为的关键工具是linkTo(),它在流中添加一个名称与第一个参数匹配的链接(在本例中为AllTopUps )。传入事件。通过导航到“投影”选项卡,选择“新建投影”,然后填写表单来运行投影,如图22.6所示。
Running the projection in Listing 22-36 causes all CreditAdded events that exist in any PayAsYouGoAccount stream to be added to a new stream called AllTopUps. This means it is possible to run queries against all the CreditAdded events without having to load all the other events in the streams like PhoneCallCharged. Event Store’s key tool for providing this behavior is linkTo(), which adds a link in the stream whose name matches the first argument (AllTopUps in this example) to the passed-in event. Projections are run by navigating to the Projections tab, choosing New Projection, and filling out the form, as per Figure 22.6.
图 22.6在 Web UI 中创建投影。
FIGURE 22.6 Creating a projection in the web UI.
图 22.6展示了正在创建的一次性投影。但是,“选择模式”字段也可以设置为“连续”。当您希望将投影应用于近实时存储的新事件时,这非常有用。CQRS 是一个常见的例子,您可能希望这样做,您很快就会看到。
Figure 22.6 shows a one-time projection being created. However, the Select Mode field can instead be set to Continuous. This is useful when you want the projection to be applied to new events as they are stored in near-real time. CQRS is a common example of where you might want to do this, as you will see shortly.
您可以在官方博客 ( http://geteventstore.com/blog/ )上了解有关 Event Store 投影功能的更多信息。此外,还有一个专门介绍投影的系列文章 ( http://geteventstore.com/blog/20130309/projections-8-internal-indexing/index.html )。
You can learn a lot more about the projection capabilities of Event Store on the official blog (http://geteventstore.com/blog/). There is a multipart series devoted to projections (http://geteventstore.com/blog/20130309/projections-8-internal-indexing/index.html).
为了提高性能和可扩展性,您可能需要创建事件的物化视图。这样做可以避免针对单个事件流持续运行大量查询。相反,每个特定需求都可以使用预先计算的值。一个演示此问题的场景是,当多个网页都针对单个事件流运行不同的查询时,如图22.7所示。
As a way to improve performance and scalability, you may want to create materialized views of your events. Doing so prevents the need for many queries to continually be run against a single event stream. Instead, precomputed values will be available for each specific need. A scenario that demonstrates this problem occurs when a number of web pages all run different queries against a single event stream, as shown in Figure 22.7.
图 22.7一个事件流用于支持许多不同的查询和用例。
FIGURE 22.7 One event stream used to support many different queries and use cases.
随着任何网页负载的增加,单个事件流承受的压力都会越来越大。这意味着不太重要的页面可能会显著降低重要页面的性能。解决这个问题的一个方法是为每个页面创建一个新的物化数据视图来支持它,如图22.8所示。
As load increases on any of the web pages, the single event stream is put under increasing amounts of stress. This means that less important pages may substantially degrade the performance of important pages. A solution to this problem is to create a new materialized view of the data to support each page, as shown in Figure 22.8.
图 22.8创建事件流的物化/非规范化视图以支持每个用例。
FIGURE 22.8 Creating materialized/denormalized views of the event stream to support each use case.
图 22.8中的解决方案是 CQRS;其中命令(写入)通过领域模型到达事件流,查询(读取)针对物化视图(也称为视图缓存)运行。您可以看到,某些用例相互妨碍的问题得到了极大缓解,因为每个用例都有自己的视图缓存用于查询。直接的好处是,您可以看到单个事件流上的争用减少了;现在,针对它运行的繁琐的临时查询更少了(如果有的话)。
The solution in Figure 22.8 is CQRS; where commands (writes) go via the domain model to the event stream, and queries (reads) are run against the materialized views (also known as view caches). You can see that the problem of certain use cases impeding others is heavily mitigated because each has its own view cache for querying. As a direct benefit, you can see contention on the single event stream has been lessened; now fewer (if any) taxing ad hoc queries are run against it.
本节仅简要介绍 CQRS 及其与事件溯源的协同作用。第 24 章“CQRS:有界上下文的架构”将更深入地讨论 CQRS,包括如何将其应用于各种不涉及事件溯源的场景。
This section is just a quick glimpse of CQRS and how it synergizes with event sourcing. In Chapter 24: “CQRS: An Architecture of a Bounded Context,” you will see a more in-depth discussion of CQRS, including how you can apply it to a variety of scenarios that don’t involve event sourcing.
要使用事件源实现 CQRS,您需要某种方式从事件流中创建非规范化视图。答案是使用之前介绍过的投影。图 22.8中的物化视图是投影可用于 CQRS 的示例。
To implement CQRS with event sourcing, you need some way to create denormalized views from your event streams. The answer is to use projections that were previously introduced. The materialized views in Figure 22.8 are examples of where projections can be used for CQRS.
正如本章前面所见,如果您使用像 Event Store 这样的专用工具,那么您已经具备使用投影和实现 CQRS 的功能。但是,当您构建自己的事件存储时,您需要自己创建该功能,这可能并非易事。幸运的是,RavenDB 提供了创建投影的功能(http://ayende.com/blog/4530/raven-event-sourcing)。
As you’ve seen previously in this chapter, if you’re using a purpose-built tool like Event Store, you already have the functionality to use projections and implement CQRS. However, when building an event store of your own, you need to create the functionality yourself, which might not be a trivial task. Fortunately, RavenDB provides the capability to create projections (http://ayende.com/blog/4530/raven-event-sourcing).
尽管 CQRS 和事件溯源是独立的概念,可以独立使用,但大量开发人员发现将它们结合起来会带来显著的优势。图 22.6展示了如何轻松地将视图缓存创建为事件流的投影。此外,它们还具有其他协同效益。
Although CQRS and event sourcing are standalone concepts that you can use independently of the other, a strong following of developers find a significant advantage from combining them. Figure 22.6 illustrated the ease at which you can create view caches as projections from event streams. But there are other synergistic benefits, too.
第十三章“通过 HTTP 与 RPC 和 REST 集成”中的一个示例展示了 Event Store 如何将事件流公开为超媒体 Atom feed。这是使用消息总线的替代方案。同样,这样做的好处是,事件存储省去了使用队列的麻烦;同样,它也省去了使用其他技术进行视图缓存和进行非规范化处理的时间。
One of the examples in Chapter 13: “Integrating via HTTP with RPC and REST,” showed how Event Store exposes event streams as a hypermedia Atom feed. This was an alternative to using a message bus. Again, the benefit of this is that your event store saves your having to use a queue; likewise, it saves your having to use another technology for view caches and another for the denormalization process.
由于事件存储是您的主要数据源、用于预测的工具以及队列技术,因此无需担心两阶段提交(或分布式事务)。一旦事件进入流中,它就成功进入队列并被成功保存。相比之下,如果您将事件存储在数据库中,使用消息总线发布事件,并更新另一个数据库中的视图缓存,而所有这些都在同一个事务中进行,那么您可能需要仔细规划如何处理这些操作中的任何一个失败,例如回滚其他操作。Greg Young 在他的博客 ( http://codebetter.com/gregyoung/2010/02/13/cqrs-and-event-sourcing/ ) 上对此进行了进一步的详细说明。
Because an Event Store is your main source of data, the tool you use for projections, and your queueing technology, there are no two-phase commits (or distributed transactions) to worry about. Once an event is in a stream, it is successfully in the queue and has successfully been saved. In contrast, if you were storing events in a database, publishing them with a message bus, and updating a view cache in another database all inside the same transaction, you may have to carefully plan how a failure in either of these actions would be handled, such as rolling back the others. Greg Young further details this on his blog (http://codebetter.com/gregyoung/2010/02/13/cqrs-and-event-sourcing/).
如果您是第一次接触事件溯源,可能仍在处理一些细节并构建思维模型。这完全可以理解,每个人都会经历这种情况。本节将概述事件溯源可以为您的项目带来的一些好处。希望即使您不记得所有细节,您仍然能够了解何时应该考虑使用事件溯源。
If this is your first encounter with event sourcing, you may still be processing some of the details and building a mental model. That’s completely understandable and something that everyone goes through. This section recaps some of the benefits that event sourcing can bring to your projects. Hopefully, at least if you don’t remember all the details, you will still have an understanding of when you might want to consider using event sourcing.
本章首先讨论了使用事件溯源的基本原理:它不仅能够分析企业自身数据,还能分析形成当前数据状态的整个行为历史,从而帮助企业获得竞争优势。如果两家公司在市场上竞争,其中一家能够依靠事件溯源进行强大的分析,那么它或许能够利用这些知识来加快其产品或服务的开发。
This chapter began by discussing the fundamental rationale for using event sourcing: allowing the business to gain a competitive advantage by being able to analyze not just its data, but the entire history of behavior that produced the current state of its data. If two companies are competing in a market and one is able to lean on event sourcing to perform powerful analysis, it may be able to use the knowledge to expedite development of its products or services.
与领域专家沟通是应用 DDD 并实现其优势的重要方面。为了支持这一点,重要的是要有一个使用通用语言 (UL) 表达概念模型的领域模型。正如你在本章前面所见,事件源聚合几乎是声明式的,每个When()的重载读起来都像一句话:
Communicating with domain experts is a significant aspect of applying DDD and realizing its benefits. To support this, it’s important to have a domain model that expresses the conceptual model using the ubiquitous language (UL). As you saw earlier in this chapter, event-sourced aggregates are almost declarative, with each overload of When() reading like a sentence:
When {Domain Event} {Apply Business Rules}
When {Domain Event} {Apply Business Rules}
这使得领域模型在知识研讨环节中更加有用。即使在一般情况下,它也能更清晰地向其他可能不熟悉该领域或代码库的开发人员传达信息。
This makes the domain model even more useful in knowledge-crunching sessions. Even in general, it can speak much more clearly to other developers who might be new to the domain or codebase.
使用对象关系映射器 (ORM) 持久化聚合一直以来都是一个备受争议的话题,并且由于阻抗不匹配而成为许多软件团队的痛点。正如您在本章中所见,从事件流中持久化和补充事件不会受到这个问题的影响,因为不存在阻抗不匹配。这意味着 ORM 技术不会限制数据库模型,也不需要复杂的映射。事实上,借助事件溯源,您完全有可能通过多态性来更改持久化技术。您在本章中看到了IEventStore的多种实现可以根据需要进行切换。
Persisting aggregates with Object Relational Mappers (ORMs) has traditionally been a topic of controversy and a source of pain for many software teams due to the impedance mismatch. As you saw in this chapter, persisting and rehydrating events from an event stream do not suffer from this problem because there is no impedance mismatch. This means that the ORM technology does not constrain the database model or require complex mappings. In fact, with event sourcing, there is a real possibility that you can change the persistence technology with polymorphism. You saw in this chapter that multiple implementations of IEventStore can be switched as necessary.
值得注意的是,IEventStore抽象确实存在一些小漏洞,因此无法保证您可以轻松地将一个事件存储提供程序替换为另一个。一个例子就是流名称;Event Store 的fromCategory()基于名称中的最后一个连字符。不过,您确实看到,在GetEventStore中有一个简单的解决方法。
It’s worth noting that the IEventStore abstraction does have small leaks, so there’s no guarantee you can easily switch out an event storage provider with another. One example is the stream names; Event Store’s fromCategory() is based on the last hyphen in the name. You did see, though, that there was an easy workaround for this in GetEventStore.
在本章前面,您了解了事件溯源如何使系统更易于调试。仔细想想,事件存储中存储了完整的行为历史记录。因此,您可以轻松地重新运行任何事件序列,以找出哪个事件导致了错误的状态更改或其他类型的错误。您无需猜测可能导致问题发生的事件序列。
Earlier in the chapter you saw how event sourcing can make systems easier to debug. If you think about it, you have the entire history of behavior stored in your event store. So you can easily rerun any sequence of events to work out which event caused an incorrect state change or some other type of bug. You don’t have to guess about the sequence of events that may have led to a problem occurring.
务必认清事件溯源的弊端。就连活动商店的供应商也建议人们仔细考虑如何使用它。你需要投入额外的时间,而且回报可能并不总是值得的——尤其是因为有很多不同的考量可能会占用你的时间。
It’s important to be realistic about the negative aspects of event sourcing, too. Even event store vendors are advising people to think carefully about where to use it. You need to invest additional time, and the payback may not always be worth it—especially because there are a number of different considerations that may be demanding of your time.
随着您对领域了解的加深,并不断改进正在构建的产品,需要将新的概念和信息添加到领域模型中。因此,您可能需要重命名事件、在事件之间移动数据,或执行其他一些会改变事件格式的更改。这会造成很大的问题,因为您已经有一个包含旧格式事件的事件流。这个问题有解决方案,而且并不总是需要花费太多精力。然而,如果您不够重视版本控制,它肯定会成为一个问题。
As you learn more about the domain and you continue to enhance the product(s) you are building, new concepts and information need to be added to the domain model. As a result, you may need to rename events, move data between events, or perform some other change that alters the format of your events. This presents a big problem, because you will already have an event stream containing events in the old format(s). There are solutions to this problem, and they don’t always require much effort. However, versioning can definitely be a problem if you don’t pay it enough respect.
投影、时态查询、快照——许多概念对于首次选择使用事件溯源的团队来说可能很陌生。与许多事情一样,你需要将理论与实践相结合才能精通,因此你应该为团队的……做好准备。短期内开发速度可能会有所放缓。您也可以考虑分配时间用于学习和实验。每当有不熟悉事件溯源的新人加入团队时,这些成本可能会有所增加。
Projections, temporal queries, snapshots—many concepts may be new to a team that is choosing to use event sourcing for the first time. As with many things, you need to combine theory with practical experimenting before you become proficient, so you should be prepared for your team’s rate of development to be slower in the short term. You may also consider allocating time for learning and experimentation. These costs may apply to some extent anytime someone new joins the team who is unfamiliar with event sourcing.
您可以重用现有的数据库技术(例如 SQL Server)进行事件溯源,但许多人可能会选择使用专门构建的事件存储。这样做的成本在于需要花费额外的时间来学习这项技术——不仅仅是使用它,还包括在实时服务器上运行它并监控其行为和资源使用情况。虽然很难量化具体成本,但首次将事件存储投入生产几乎肯定需要投入人力和时间。
You can reuse existing database technologies like SQL Server for event sourcing, but it’s likely that many will choose to use a purpose-built event store. The cost of this is the extra time it takes to learn the technology—not just using it, but running it on live servers and monitoring its behavior and resource usage. It’s hard to quantify just how much, but putting an event store into production for the first time almost certainly requires an investment in people hours.
显然,存储导致该状态的整个活动历史记录需要在磁盘上存储比仅存储状态更多的信息。幸运的是,如今存储成本非常低廉,很多人可能不太担心这个问题。然而,您仍然应该牢记这一点,并进行相应的监控。
It’s obvious that storing the entire history of activity that led to the state requires more information to be stored on disk than just storing state. Fortunately, storage is incredibly cheap nowadays, and this is unlikely to be a concern for many. However, it is something that you should keep in mind and monitor accordingly.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 23 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 23 download and individually named according to the names throughout the chapter.
客户最关心的是应用程序的用户界面。如果它看起来引人注目,并且能够帮助他们实现目标,比如找到完美的假期,他们就会乐意为此花费一大笔钱。但设计出优秀的用户界面不仅仅是让设计师设计出赏心悦目的效果。在性能、可扩展性和后台有界上下文的松耦合方面,存在着重大的工程挑战。
Customers mostly care about the user interface of your application. If it looks compelling and allows them to achieve what they want, such as finding the perfect holiday, they will be happy to spend lots of money. But getting the UI right is more than just about letting designers come up with eye candy. There are significant engineering challenges tied to the performance, scalability, and loose coupling of your behind-the-scenes bounded contexts.
UI 的一项基本工程挑战是整合所有数据。对于电商应用,你可能希望在一个页面上显示目录项、价格、配送选项、特价优惠以及其他类型的信息。你从本文第二部分中已经了解在《战略模式:在有界上下文之间进行通信》一书中,我们了解到,在事件驱动的应用程序中,这些信息存储在多个最终一致的有界上下文中。您也知道这些类型的系统是无共享的;换句话说,Web 应用程序不能简单地查询另一个有界上下文的数据库,因为这会增加耦合度。为了解决这个问题,您可以选择各种方案,每种方案都有各种权衡取舍。例如,您可以在服务器上组合数据,或者直接在网页中通过 AJAX 调用组合数据。您的有界上下文可以选择返回纯数据(通常是 XML 或 JSON),也可以返回可直接转储到页面上的 HTML。本章提供了每种方案的示例,以及有关每种模式何时适用以及涉及哪些权衡取舍的指导。
One of the fundamental engineering challenges of a UI is pulling together all the data. For an e-commerce application, you may want to show catalog items, prices, shipping options, special offers, and other types of information on a single page. You know from Part II of this book, “Strategic Patterns: Communicating Between Bounded Contexts” that with event-driven applications, this variety of information is stored in multiple, eventually consistent bounded contexts. You also know that these types of systems are share-nothing; in other words, the web application cannot simply query the database of another bounded context because that increases coupling. To solve this problem, you have choices, each with a variety of trade-offs. For instance, you can combine the data on the server or via AJAX calls directly in the web page. Your bounded contexts have the option of returning plain data, usually XML or JSON, or they may return HTML that can be directly dumped onto the page. This chapter has examples of each of these scenarios, along with guidance about when each pattern is relevant and what trade-offs are involved.
不过,在开始示例之前,本章首先会带你了解一些主要的 UI 考量,从高层决策(例如应该由哪个团队负责)到底层决策(例如应该使用哪种编程语言)。完成本章后,你将了解应用层如何处理来自 UI 的输入,以及如何提供所有基础架构粘合剂来协调有界上下文的操作。
Before commencing with the examples, though, this chapter begins by taking you through some of the main UI considerations from high-level decisions—such as which team should own it—to low-level decisions—like which programming language to use. After completing this chapter, you will learn about how the application tier deals with inputs coming from the UI and provides all the infrastructural glue to coordinate actions with bounded contexts.
在设计从多个有界上下文引入内容的 UI 时,你可能会惊讶地发现其中涉及了如此多的选项和权衡。有些选项可能会影响你设计后端应用程序编程接口 (API) 的方式,而其他选项甚至可能会影响你对编程语言的选择。事实上,你的 UI 甚至可能会影响哪些数据需要由某些有界上下文存储。
It can be quite surprising to see the variety of options and trade-offs that are involved in designing UIs that bring in content from multiple bounded contexts. Some of the options may affect how you design your back-end application programming interfaces (APIs), whereas others may even impact your choice of programming language(s). In fact, your UI could even affect which data needs to be stored by some bounded contexts.
设计 UI 时,首先要确定谁在逻辑上拥有它。例如,它可以位于单个有界上下文(更具体地说是单个业务组件)中,并由负责该有界上下文的团队拥有。或者,视图可以从多个有界上下文中提取数据,但不属于任何一个有界上下文。
Your first decision when designing a UI is to decide who logically owns it. For instance, it could live within a single bounded context (more specifically a single business component) and be owned by the team responsible for that bounded context. Alternatively, the view could pull in data from multiple bounded contexts but not be owned by any of them.
自治应用程序的 UI 属于单个业务组件。它不需要从其他有界上下文中拉取内容。然而,这意味着业务组件需要将所有应该在 UI 上呈现的信息存储在本地。为了实现这一点,业务组件必须订阅来自其他包含所需数据的有界上下文的事件,并将数据存储在本地。这在第二部分中已经讨论过,如图 23.1所示。
A UI for an autonomous application belongs to a single business component. It does not need to pull in content from another bounded context. However, this means the business component needs to store locally all the information that should be presented on the UI. To make this possible, the business component has to subscribe to events from other bounded contexts that contain the data it needs and store the data locally. This was discussed previously in Part II and is illustrated in Figure 23.1.
图 23.1自主应用程序的 UI。
FIGURE 23.1 UI for autonomous applications.
图 23.1展示了一个由 Catalog 限界上下文负责的内容增强应用程序。它允许目录团队的人员更新和覆盖特定产品的内容。所有产品内容都存储在此业务组件中,因此完全可用。然而,在更新内容时,业务人员希望了解产品的销售频率,以便了解应该在内容质量上投入多少精力。通过订阅销售边界上下文的销售完成事件来检索此信息并将其存储在内容增强数据库中,准备在自主 Web 应用程序的 UI 中呈现。
Figure 23.1 illustrates a content-enhancement application that the Catalog bounded context owns. This allows people working in the catalog team to update and override the content for specific products. All the content for products is stored in this business component, so it is fully available. However, when they’re updating content, the business staff members want to know how often a product is sold so they can understand how much effort they should put into the quality of content. This information is retrieved by subscribing to the Sales bounded context’s Sale Completed event and stored in the Content Enhancement database, ready to be presented in the autonomous web application’s UI.
在自主应用程序中,最终一致性对于 UI 来说可能是一个重要的考虑因素,因为显示的信息可能并非完全最新。对于图 23.1中的示例,销售数量即使晚几分钟、几小时甚至几天都没有问题,因为目录管理员只需要了解某个商品的受欢迎程度。但如果数据新鲜度是一个重要考虑因素,那么权威应用程序可能是更好的选择。
Eventual consistency can be an important consideration with UIs in autonomous applications because the information shown may not be fully up to date. For the example in Figure 23.1, it is fine for the number of sales to be minutes, hours, or even days out of date because the catalog staff just need an idea of an item’s popularity. But if data freshness was a big concern, an authoritative application may be a better choice.
当你想在单个 UI 中获取来自多个有界上下文的最新信息快照时,应用程序必须直接从权威有界上下文请求每条信息。你可以在图 23.2中看到这一点。
When you want the latest snapshot of information from multiple bounded contexts in a single UI, the application has to request each piece of information directly from the authoritative bounded context. You can see this in Figure 23.2.
图 23.2服从权威的 UI。
FIGURE 23.2 UI that defers to authority.
图 23.2展示了一个电商网页,它调用多个有界上下文,每个上下文都对应着所需信息的权限,用于获取特价、价格和其他类型的信息。每次请求页面时都会发生这种情况,因此信息是完全最新的,而非最终一致的。
Figure 23.2 shows an e-commerce web page that calls into multiple bounded contexts, each the authority for the desired information, to get special offers, prices, and other kinds of information. This happens each time the page is requested, so the information is fully up to date, not eventually consistent.
遵循每条信息权威的 UI 不属于有界上下文。相反,许多公司拥有专门的 Web 团队,他们不拥有有界上下文,但完全负责网站。如果这种方法不适合您,您可以让拥有有界上下文的团队也负责遵循权威的 UI。重要的是要记住,从概念上讲,UI 不属于他们的有界上下文。
UIs that defer to the authority of each piece of information do not belong to a bounded context. Instead, many companies have dedicated web teams that don’t own bounded contexts but are completely responsible for the website. If that approach doesn’t work for you, you can let teams that own a bounded context also be responsible for UIs that defer to authority. It’s just important to remember that conceptually the UI does not belong to their bounded context.
在自主型和权威型应用程序之间进行选择时,一个好的起点是考虑团队关系。如果 UI 是为特定部门设计的,例如内部工具,那么将其保留在该团队内部可能更高效。另一方面,如果 UI 是包含许多 UI 的大型应用程序的一部分,例如公共网站,您可能需要一个专门的 Web 团队来处理所有 Web 和前端挑战。
A good starting point for choosing between autonomous and authoritative applications is to think about team relationships. If the UI is for a specific department, like an internal tool might be, it may be more efficient to keep it within that team. On the other hand, if the UI forms part of a bigger application that contains many UIs, such as a public website, you may want a dedicated web team to deal with all the web and front-end challenges.
另一个需要考虑的因素是,让自治应用程序在本地存储所有所需数据,需要额外的数据存储量和复杂性。如果对于一个相对较小的用例来说,这需要大量的额外工作,那么权威性方案可能会以最少的短期和长期投入获得最大的收益。但如果您确实需要完全最新的信息,最终一致的自治应用程序可能并非最佳选择。
Another consideration is the amount of extra data storage and complexity involved in enabling an autonomous application to have all the data it needs locally. If it is a lot of extra work for a relatively minor use case, the authoritative option might provide the most benefit for the least amount of short- and long-term effort. But if you do need fully up-to-date information, eventually consistent autonomous applications are probably not the best choice.
通过使用从每个有界上下文返回的 HTML 片段构建网页,您可以让有界上下文控制页面特定区域的外观和行为,如图23.3所示。
By constructing web pages with snippets of HTML that are returned from each bounded context, you give bounded contexts control of the appearance and behavior of specific regions of a page, as Figure 23.3 shows.
图 23.3使用有界上下文提供的 HTML 编写网页。
FIGURE 23.3 Composing a web page with HTML provided by bounded contexts.
另一种选择是让页面仅从限界上下文中提取数据,这种方式对限界上下文的呈现控制较少。使用这种方法,您可以在一个位置管理所有呈现关注点,如图 23.4所示。
Another option, which offers less presentational control to bounded contexts, is to have pages only pull in data from bounded contexts. With this alternative approach, you can manage all the presentation concerns in a single location, as shown in Figure 23.4.
图 23.4从多个有界上下文中提取数据。
FIGURE 23.4 Pulling in data from multiple bounded contexts.
大多数在线经验报告表明,第二种方法是迄今为止最突出的,并且通常以 JSON API 的形式表示。但两种方法都可以。一个重要的考虑因素是您是否提供外部使用的 API。在第 13 章“通过 HTTP 与 RPC 和 REST 集成”中提到,在这种情况下,对 API 进行内部测试可以带来许多好处。
Most online experience reports indicate that the second approach is by far the most prominent, and it’s usually expressed as JSON APIs. But both approaches can work. One important consideration is whether you provide APIs that are used externally. It was mentioned in Chapter 13, “Integrating Via HTTP with RPC and REST,” that dogfooding your API can have a number of benefits in such scenarios.
对于从多个有界上下文中提取内容(数据或 HTML)的 UI,可以选择在客户端或服务器上执行聚合。通过在网页内将每个请求设为 AJAX 请求,可以避免服务器端应用程序的复杂性和额外的故障点。相反,客户端上的 JavaScript 会更加复杂。现在许多团队都在构建单页应用程序 (SPA),在这种情况下,这个问题就不那么严重了。关于这个主题的一般建议倾向于客户端,但这两种方法都被广泛使用。图 23.5展示了在客户端上编织内容,而图 23.6展示了服务器端方法。
For a UI that pulls in content (data or HTML) from multiple bounded contexts, there is the choice of performing the aggregation on the client or the server. By making each request an AJAX request inside the web page, you can avoid the complexity and additional failure point of the server-side application. Conversely, you will have more complexity on the client as JavaScript. Building Single Page Applications (SPAs), as many teams are now, is one case in which this is less of a problem. The general recommendation on this topic tends to favor the client, but both approaches are in wide use. Figure 23.5 illustrates knitting together content on the client, whereas Figure 23.6 illustrates the server-side approach.
图 23.5在客户端进行聚合。
FIGURE 23.5 Aggregating on the client.
图 23.6在服务器上聚合。
FIGURE 23.6 Aggregating on the server.
即使所有有界上下文都位于同一个解决方案中并作为单个应用程序运行,UI 组合仍然有助于在有界上下文之间划分展示职责。当一个有界上下文想要更改其页面部分时,这些更改可能仅限于该有界上下文,这意味着不会干扰其他上下文。这与单一职责原则 (SRP) 的初衷相同。在本节中,您将使用 ASP.NET MVC 的RenderAction()实现此场景。您将创建一个简单的页面,从位于同一解决方案中的三个有界上下文中提取 HTML 内容,如图23.7所示。
Even when all your bounded contexts live inside the same solution and run as a single application, UI composition can still be useful for partitioning presentational responsibility among bounded contexts. When one bounded context would like to alter its portion(s) of a page, the changes may be confined to that bounded context, meaning no interference with others. This is the same intention that motivates the Single Responsibility Principle (SRP). In this section, you implement this scenario using ASP.NET MVC’s RenderAction(). You’re going to create a simple page that pulls in HTML content from three bounded contexts that live inside the same solution, as shown in Figure 23.7.
图 23.7此示例的设计。
FIGURE 23.7 The design for this example.
要开始此示例,您需要创建一个名为PPPDDD.NonDist.UIComp的新 ASP.NET Web 应用程序。选择【空】模板,并选中【MVC】复选框。此应用程序仅包含一个视图,它将是复合 UI,因此可以将其作为应用程序的初始页面。为此,在Controllers文件夹中添加一个名为HomeController的类,其内容如清单23-1所示。
To begin this example, you need to create a new ASP.NET web application called PPPDDD.NonDist.UIComp. Choose the Empty template, and check the MVC check box. This application contains only a single view, which will be the composite UI, so it’s fine for it to be the initial page of the application. To achieve this, add a class called HomeController in the Controllers folder with the content shown in Listing 23-1.
HomeController所做的就是返回一个视图,您可以通过在/Views/Home/文件夹(必须创建)中添加一个名为Index.cshtml的文件来创建该视图。添加视图后,需要将其内容替换为清单 23-2 所示的代码。
All the HomeController does is return a view, which you can create by adding a file called Index.cshtml in the /Views/Home/ folder (which you must create). Once you’ve added the view, you need to replace its contents with the code shown in Listing 23-2.
清单 23-2 展示了 UI 组合的基本实现。您可以看到,页面本身几乎只是一个模板。它通过调用RenderAction()直接渲染每个有界上下文提供的 HTML 来提取其主要内容。第一个参数是要在具有第二个参数名称的控制器上调用的方法的名称。因此,在此示例中,第一个RenderAction()调用CatalogBoundedContextController.ItemInBasket(),传入需要渲染的目录项的产品 ID。您可以在页面顶部看到产品 ID 列表是硬编码的。这使示例专注于 UI 方面,这不是推荐的做法。
Listing 23-2 shows a basic implementation of UI composition. You can see that the page itself is almost just a template. It pulls in its main content by directly rendering HTML provided by each bounded context by calling RenderAction(). The first argument is the name of the method to be called on a controller that has the name of the second argument. So in this example, the first RenderAction()calls CatalogBoundedContextController.ItemInBasket(), passing in the product ID of the catalog item that needs to be rendered. You can see at the top of the page that the list of product IDs is hard-coded. This keeps the example focused on the UI aspects and is not a recommended practice.
为了正确呈现此页面,必须实现每个RenderAction()调用的三个控制器方法。从第一个开始,需要将一个名为CatalogBoundedContextController的类添加到Controllers文件夹中。它应包含清单 23-3 所示的代码。
For this page to render correctly, you must implement the three controller methods to be called by each RenderAction(). Starting with the first, you need to add a class called CatalogBoundedContextController to the Controllers folder. It should contain the code shown in Listing 23-3.
从概念上讲,清单 23-3 中最重要的关注点是,此控制器通过调用其ProductFinder上的方法,从 Catalog 有界上下文中获取所需的所有信息。您不会想在这个控制器内调用其他有界上下文上的方法,因为这样会导致多个有界上下文耦合,其中一个上下文的更改可能会影响另一个上下文。这也可能导致不同的团队在尝试同时进行更改时互相干扰。
Conceptually, the most important concern in Listing 23-3 is that this controller is getting all the information it needs from the Catalog bounded context by calling methods on its ProductFinder. You wouldn’t want to call methods on other bounded contexts inside this controller because then you would have a coupling on multiple bounded contexts, where changes to one might affect the other. It might also lead to different teams getting in each other’s way as they try to make changes at the same time.
从技术上讲,重要的细节是对PartialView()的调用,它传入一个视图模型。这将返回由位于/Views/CatalogBoundedContext/ItemInBasket.cshtml(这是一个 Razor 视图页面)的部分视图页面生成的 HTML。现在可以添加该文件了。其代码如清单 23-4 所示。
Technically, the important detail is the call to PartialView(), which passes in a view model. This returns the HTML that is produced by a partial view page at the location /Views/CatalogBoundedContext/ItemInBasket.cshtml (which is a Razor view page). You can now add that file. The code for it is shown in Listing 23-4.
在清单 23-4 中,创建了一个 HTML 模板,该模板使用 Razor 语法将传入的视图模型中的值填充到占位符中。此模板生成的 HTML 直接渲染到发生ItemInBasket RenderAction() 的复合 UI 上。
In Listing 23-4, an HTML template is created that uses Razor syntax to populate placeholders with values from the passed-in view model. The HTML generated by this template is directly rendered onto the composite UI where the ItemInBasket RenderAction() occurs.
要完成此示例,您需要在Controllers文件夹中添加另外两个控制器:PricingBoundedContextController和ShippingBoundedContextController。您还需要添加分部视图:/Views/PricingBoundedContext.Price.cshtml和/Views/ShippingBoundedContext/DeliveryOptions.cshtml 。每个文件的代码如清单 23-5 至 23-8 所示。
To complete this example, you need to add the other two controllers: PricingBoundedContextController and ShippingBoundedContextController in the Controllers folder. You also need to add the partial views: /Views/PricingBoundedContext.Price.cshtml and /Views/ShippingBoundedContext/DeliveryOptions.cshtml. The code for each of these files is shown in Listings 23-5 through 23-8.
运行应用程序并导航到根 URL 时,您应该看到呈现的复合 UI,如图 23.8所示。
When running the application and navigating to the root URL, you should see the composite UI rendered, as shown in Figure 23.8.
图 23.8复合 UI 的渲染。
FIGURE 23.8 Rendering of composite UI.
创建从多个 HTTP API 提取信息的网页是 Web 开发中的常见场景。在本例中,您将学习如何从多个有界上下文(每个上下文都作为单独的应用程序运行)提取信息,并直接在浏览器中使用 JavaScript 聚合所有信息。您可以在图 23.9中看到此示例模拟的设计。
Creating web pages that pull in information from multiple HTTP APIs is a common scenario in web development. In this example, you learn how to pull in information from multiple bounded contexts—each running as a separate application—and aggregate all the information using JavaScript directly in the browser. You can see the design this example simulates in Figure 23.9.
图 23.9此示例的设计。
FIGURE 23.9 The design for this example.
在图 23.9中,需要注意的第一个关键细节是,每个 API 都位于一个对其他有界上下文一无所知的有界上下文中。您可以使用完全不同的技术编写每个有界上下文,只要它提供所需的 HTTP API 即可。另一个关键细节是每个 API 都返回 JSON。这意味着所有呈现关注点都被隔离在主网站中。
In Figure 23.9, the first key detail to note is that each API lives inside a bounded context that knows nothing about the other bounded contexts. You can write each bounded context in a completely different technology as long as it provides the required HTTP API. Another key detail is that each API returns JSON. This means all the presentation concerns are isolated in the main website.
要开始此示例,您需要创建一个名为PPPDDD.Dist.UIComp的新 ASP.NET Web 应用程序。与之前一样,创建项目时,务必选择“空”模板并勾选“MVC”复选框。创建项目后,您可以添加HomeController及其对应的视图/Views/Home/Index.cshtml,它将被自动选为应用程序的默认页面。HomeController的内容如清单 23-9 所示,Index.cshtml的内容如清单 23-10 所示。
To start this example, you need to create a new ASP.NET web application called PPPDDD.Dist.UIComp. As before, when creating the project, it is important to select the Empty template and check the MVC check box. With your project created, you can add a HomeController and its corresponding view /Views/Home/Index.cshtml, which is automatically chosen as the default page for the application. The content of HomeController is shown in Listing 23-9, and the content of Index.cshtml is shown in Listing 23-10.
在清单 23-9 中,HomeController渲染视图。您可以在清单 23-10 中看到视图的标记。请注意,这里的内容比示例 1 中的 UI 要多。这是因为有界上下文只提供数据而不提供 HTML 标记。一些标记是在从每个有界上下文的 HTTP API 异步获取数据时动态添加的。这发生在页面头部引用的pppddd-application.js JavaScript 文件中。pppddd -application.js的内容如清单 23-11 所示。您需要将此文件添加到Scripts文件夹中。
In Listing 23-9, the HomeController renders the view. You can see the markup for the view in Listing 23-10. Notice that there is more content than in the UI from Example 1. This is because the bounded contexts only provide data and not HTML markup. Some of the markup is dynamically added as the data is asynchronously fetched from each bounded context’s HTTP API. This happens inside the pppddd-application.js JavaScript file that is referenced in the head of the page. The content of pppddd-application.js is shown in Listing 23-11. You need to add this file to the Scripts folder.
在pppddd-application.js中,页面加载时会向每个有界上下文发出 JSON Web 请求。收到 JSON 响应后,页面的相关部分会进行更新。一个重要的细节是createHolidayView()函数。请注意它如何生成将在页面上渲染的 HTML。这表明所有呈现关注点都保留在 Web 应用程序内部,而不是像 API 返回 HTML 那样分散在每个有界上下文中。(这本身无所谓好坏;您需要根据权衡利弊做出明智的选择。)
In pppddd-application.js, a web request for JSON is made to each bounded context when the page loads. When the JSON response is received, relevant parts of the page are updated. One important detail is the createHolidayView() function. Notice how it generates HTML that will be rendered on the page. This demonstrates that all presentation concerns remain inside the web application as opposed to being scattered among each bounded context as would be the case if the APIs returned HTML. (This is neither better nor worse; it’s a conscious choice you need to make based on the trade-offs.)
您的 UI 现已完成。唯一缺少的是每个有界上下文提供的 HTTP API。在真实的分布式系统中,每个 API 都将作为单独的应用程序在不同的有界上下文中运行。但在这个例子中,它们位于同一个 Web 应用程序中,以简化您的工作。要将这些 API 添加到项目中,您需要在Controllers文件夹中添加HolidaysController、PromotionsController和RecommendationsController。每个类的代码如清单 23-13 至 23-15 所示。
Your UI is now complete. All that is lacking is an HTTP API that each bounded context provides. In a real distributed system, each of those APIs would be running as a separate application inside a different bounded context. But in this example, they live inside the same web application to make your life easier. To add those APIs to the project, you need to add a HolidaysController, a PromotionsController, and a RecommendationsController in the Controllers folder. The code for each of these classes is shown in Listings 23-13 through 23-15.
如您所见,每个控制器都执行了最低限度的操作,以所需格式返回 JSON(参见清单 23-12)。在实际应用程序中,这正是应用服务控制的地方。正如您将在下一章中了解到的,应用服务位于领域和外部契约(例如 HTTP API)之间,用于根据来自 UI 的数据或发送到 UI 的数据来协调操作。在进入下一章之前,如果您在项目中按下 F5 键,您应该会看到如图 23.10所示的页面渲染效果。
As you can see, each controller does the bare minimum to return JSON in the required format (see Listing 23-12). In a real application, this is where your application services take control. As you’ll learn in the next chapter, application services sit between the domain and external contracts, such as an HTTP API, to coordinate actions based on data coming in from the UI or going out to the UI. Before moving on to the next chapter, if you press F5 inside your project, you should see the page rendered as per Figure 23.10.
图 23.10客户端 JSON API 组合的实际操作。
FIGURE 23.10 Client-side JSON API composition in action.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for this Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 24 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 24 download and individually named according to the names throughout the chapter.
CQRS(命令查询职责分离)是一种可应用于有界上下文的简单模式。它将领域模型分为两个模型:读取模型和写入模型(有时也称为事务模型)。
CQRS (Command Query Responsibility Segregation) is a simple pattern that you can apply to a bounded context. It separates the domain model into two models: a read model and a write model (sometimes called a transactional model).
分离的目的是使模型能够毫不妥协地满足单一上下文的需求。这两个上下文分别负责报告领域状态和执行业务任务,也称为读写端。对于具有复杂呈现需求和丰富领域逻辑的有界上下文,使用单一模型通常会导致该模型过于复杂且缺乏完整性,从而给领域专家带来困惑,并给开发人员带来维护方面的噩梦。通过应用 CQRS 模式,可以将模型一分为二,从而优化每个模型,使其能够更有效地服务于每个上下文。
The reason for the separation is to enable a model to serve the needs of a single context without compromise. The two contexts in question are reporting on the state of the domain and performing business tasks, also known as the read and write sides. Using a single model for bounded contexts that have complex presentation needs and rich domain logic often results in that model becoming overly complex and devoid of integrity, generating confusion for domain experts and a maintenance nightmare for developers. By applying the CQRS pattern, a model is split in two, enabling each model to be optimized to serve each context more effectively.
CQRS 并非顶层架构;它是一种处理复杂性的模式,可以应用于需要支持与事务模型结构不一致的展示模型的有界上下文。大多数 Web 应用程序在查询和命令之间存在差异。CQRS 将这两者分离开来,使两者能够进行优化,且不影响性能。
CQRS is not a top-level architecture; it is a pattern for handling complexity that can be applied against bounded contexts needing to support a presentation model that is not aligned to the structure of the transactional model. The majority of web applications see a disparity between queries and commands. CQRS splits these two and enables the sides to be optimized without compromise.
本章将介绍 CQRS 模式,帮助您全面了解其有效应用场景。第 25 章“命令:用于处理业务用例的应用服务模式”和第 26 章“查询:领域报告”将深入探讨该架构中命令端和查询端的实现。
This chapter introduces you to the CQRS pattern, giving you a holistic understanding of where it can be effective. Chapters 25, “Commands: Application Service Patterns for Processing Business Use Cases,” and Chapter 26, “Queries: Domain Reporting,” go deeper into the implementation of the command and query sides of the architecture.
图 24.1展示了一个典型的有界上下文分层架构。该架构的核心是领域模型。领域模型的创建是为了在处理事务操作时强制执行领域的不变量。该模型由为保持一致性而构建的小型聚合对象组组成,这些对象表达了领域的规则和逻辑。然而,应用程序的报告需求可能与聚合的结构不一致,导致应用服务需要加载许多不同的聚合来构建视图模型,而这些视图模型可能只包含在聚合实例中检索到的数据子集。这种视图生成很快就会变得复杂且难以维护,在最糟糕的情况下,甚至会降低系统速度。
Figure 24.1 shows a typical layered architecture of a bounded context. Within the heart of this architecture lies a domain model. The domain model is created to enforce the invariants of the domain when handling transactional operations. The model is composed of small aggregate groupings of domain objects built for consistency and that express the rules and logic of the domain. The reporting needs of an application, however, may not be aligned with the structure of the aggregates, resulting in application services needing to Load many different aggregates to construct view models that may contain only a subset of the data that is retrieved within the aggregate instances. This view generation can quickly become complex and difficult to maintain and in the very worst scenarios can slow down the system.
图 24.1满足应用程序读取和写入功能的单一模型。
FIGURE 24.1 A single model fulfilling the read and write sides of an application.
为了支持视图生成,领域模型需要公开内部状态,并需要添加与领域不变量关系不大的展示属性。存储库通常在契约中包含许多额外的方法,以支持分页、查询和自由文本搜索等展示需求。由于应用程序的读取端通常比写入端使用更频繁,用户经常寻求改进报告生成功能。为了简化查询端并提高其性能,模型需要做出一些妥协。聚合被合并,并使用延迟加载来避免提取那些对于事务性业务任务处理来说并非必需但对于展示目的却必不可少的数据。这导致单一模型充满了妥协,并且在读取和写入方面都不符合标准。
To support view generation, domain models need to expose internal state and need to be adorned with presentation properties that have little to do with the invariants of the domain. Repositories often contain many extra methods on the contract to support presentation needs such as paging, querying, and free text searching. Because the read side of an application is typically used more frequently than the write side, users often seek improvements in report generation. To try to simplify as well as improve performance of the query side, the model is compromised. Aggregates are merged, and lazy loading is used to prevent pulling data that is not required for transactional business task processing needs, but that is required for presentational purposes. This leads to a single model that is full of compromises and is sub-standard for both reading and writing.
图 24.2展示了一个采用 CQRS 模式的架构。它通过提供两个模型(而不是一个)来分别处理两个相互冲突的上下文(即读取和写入)的需求。现在,每个模型都可以针对其服务的特定上下文进行优化,同时保持其概念的完整性。从某种意义上说,您是在较低层次上应用有界上下文模式,即通过为读取上下文绑定一个模型,并为写入上下文绑定另一个单独的模型。
Figure 24.2 demonstrates an architecture that employs the CQRS pattern. It treats the needs of the two conflicting contexts—namely, reads and writes—separately by providing two models instead of one. Each model can now be optimized for the specific context it serves while retaining its conceptual integrity. You are in a sense applying the bounded context pattern at a lower level by binding one model for the read context and a separate model for the write context.
图 24.2具有单独读写模型的 CQRS 模式。
FIGURE 24.2 The CQRS pattern with a separate read and write model.
图 24.2展示了命令与查询之间的分离:命令负责执行客户端调用的业务任务,查询负责执行客户端请求的报告。在图 24.2中,架构的读取端和写入端使用相同的数据存储。这并非强制性的;可以使用单独的读取存储来扩展读取端。
Figure 24.2 shows the segregation between commands; the responsibility to fulfill business tasks, which are invoked by a client, and queries and the responsibility to fulfill reports, which are requested by a client. In Figure 24.2, the same data store is used for both the read and the write side of the architecture. This is not mandatory; a separate read store can be employed to scale the read side.
架构的命令端负责维护领域规则。它代表满足业务任务的领域逻辑。图 24.3所示的架构乍一看与典型的分层方法相同;然而,命令端不支持查询,任何响应仅仅是对客户端发起的业务任务成功的确认。
The command side of the architecture is concerned with upholding the rules of the domain. It represents the domain logic that satisfies business tasks. The architecture shown in Figure 24.3 at first glance is the same as the typical layered approach; however, the command side does not support querying, and any responses are merely acknowledgements on the success of the business task a client initiates.
图 24.3 CQRS 的命令端。
FIGURE 24.3 The command side of CQRS.
命令是一项业务任务,是系统的一个用例,它位于应用层。您应该使用业务语言编写命令。这不是UL(业务实践语言);它是用来描述系统行为的语言,而不是领域模型的术语和概念。通常,如果您遵循BDD方法,命令会来自您编写的用例和故事。
A command is a business task, a use case of a system, and it lives within the application layer. You should write commands in the language of the business. This is not UL; it is the language that captures the behaviors of the systems rather than the terms and concepts of the domain model. Typically, if you are following a BDD approach, commands come from the use cases and stories you produce.
您应该将命令建模为动词而不是名词。它们应该明确地捕捉用户的意图。清单 24-1 中展示了一个命令示例。命令是一个带有简单参数验证的简单数据传输对象 (DTO)。
You should model commands as verbs rather than nouns. They should capture the intent of the user explicitly. An example of a command is shown in Listing 24-1. A command is a simple data transfer object (DTO) with simple parameter validation.
从清单 24-1 可以看出,命令名称揭示了用户的意图。在本例中,它是为了让客户兑换礼券。该命令表示对要执行的业务任务的请求,因此用现在时态书写(例如:我想做某事),而不是用过去时态书写的领域事件(例如:发生了某事)。
As you can see from Listing 24-1, the command name reveals the intent of a user. In this case, it is for a customer to redeem a gift certificate. The command represents a request for a business task to be actioned and is therefore written in the present tense (for example: I want to do something) as opposed to domain events, which are written in past tense (for example: Something happened).
满足应用程序展示和过渡需求的模型通常与该应用程序的用户界面相似。聚合的形状从处理不变量转变为与用户界面匹配的结构。例如,以图 24.4中的用户界面模型为例。这个类似仪表板的屏幕显示了客户的各种属性。
A model that serves both the presentational and the transitional needs of an application often resembles the user interfaces of that application. The shape of aggregates is morphed from handling invariants into structures that match the user interface. For example, take the mockup of a user interface in Figure 24.4. This dashboard-like screen presents various attributes of a customer.
图 24.4从多个聚合中提取数据的用户界面。
FIGURE 24.4 A user interface pulling data from many aggregates.
清单 24-2 展示了经常创建的领域对象类型,以满足演示的需要,同时实现领域的逻辑。
Listing 24-2 shows the type of domain object that is often created to meet the needs of the presentation while also implementing the logic of the domain.
现在可以轻松地为表示模型生成视图,因为领域模型与用户界面完全一致。这只是检索完整的客户聚合并将其映射到视图模型的一个示例,如清单 24-3 所示。
A view is now easy to generate for the presentation model because the domain model is in complete alignment with the user interface. It is simply a case of retrieving the full customer aggregate and mapping it to a view model, as demonstrated in Listing 24-3.
然而,聚合现在非常庞大,并且负责与客户相关的所有事务。聚合是围绕 UI 而非领域不变量构建的。换句话说,聚合基于报表屏幕而非领域行为。您可以通过应用 CQRS 并将模型从任何展示需求中解放出来来避免这些问题。在命令模型中,创建单一的客户概念毫无益处,这通常被视为代码异味,并被称为“上帝对象”。取而代之的是,将有一个聚合负责管理忠诚度规则,一个单独的聚合负责客户详细信息,以及第三个聚合负责礼券余额。UL 比 UI 屏幕更适合应用程序的行为。领域专家会讨论行为和规则,而不是 UI。
However, the aggregate is now very large and is responsible for anything associated with a customer. The aggregate is structured around the UI rather than the invariants of the domain. In other words, the aggregates are based on a report screen instead of domain behavior. You can avoid these issues by applying CQRS and freeing the model from any presentational requirements. In the command model, there is no benefit in creating a single concept of a customer, often seen as a code smell and referred to as the god object. Instead, there will be an aggregate responsible for the rules governing loyalty, a separate aggregate for customer details, and a third for gift certificate balance. The UL will fit better around the behaviors of application than the UI screen. Domain experts will talk about behavior and rules, not about UI.
没有了这些额外的职责,命令模型可以变得更小,更专注于应用服务的行为,聚合也可以变得更简洁。由于聚合检索仅受 ID 限制,而不是像分页和排序这样的一系列查询方法,因此存储库可以得到极大简化。开发人员可以围绕不变量对聚合进行建模,并专注于事务行为,而不受演示需求的干扰。
Without its added responsibilities the command model can be smaller and more focused on behavior with application services, and aggregates can become more concise. Repositories can be massively simplified as aggregate retrieval is restricted by ID rather than a host of querying methods such as paging and sorting. Developers can model aggregates around invariants and focus on transactional behavior without the noise of the presentation needs.
命令处理程序是应用服务的一种。它负责处理命令,并包含协调任务完成的逻辑。这些逻辑可以包括委托给领域模型、持久化和检索,以及调用基础设施服务(例如支付网关的电子邮件客户端)。
A command handler is a flavor of an application service. The handler processes the command and contains logic to orchestrate the completion of a task. This logic can include delegation to the domain model, persistence and retrieval, and calling out to infrastructure services such as e-mail clients of payment gateways.
命令处理程序仅返回命令成功或失败的确认;不应使用它来查询或报告域状态。清单 24-4 给出了一个命令处理程序的示例。下一章将详细介绍如何实现命令处理程序模式。
A command handler only returns an acknowledgement of the success or failure of a command; you should not use it to query or report in the domain state. Listing 24-4 presents an example of a command handler. Details of how to implement the command handler pattern are covered in the next chapter.
由于命令端的领域模型是为了实现领域规则和逻辑而构建的,因此它无需包含不必要的展示性属性。DDD 聚合支持命令处理,而非建模实际场景。处理程序可以帮助聚合专注于行为和不变量,而非实际场景。处理特定命令时,它们不需要完整的领域实体。(也就是说,对客户聚合执行某些操作时,它们不需要客户名称。)客户实体没有意义。这有助于保持聚合规模较小。这又有助于您使用更少的关联来建模更小的聚合。
Because a domain model on the command side is built to implement domain rules and logic, it does not need to contain unnecessary presentational properties. DDD aggregates support command processing rather than model real life. Handlers can help to focus aggregates on behavior and invariants rather than on real life. Specific commands don’t need full domain entities when you’re handling them. (That is, they don’t need the customer name when you’re performing some action on the customer aggregate.) Customer entity doesn’t make sense. This helps keep aggregates small. This again aids you in modeling smaller aggregates with fewer associations.
如图 24.4所示,查询端的架构与领域报告相关。查询端返回的对象是简单的 DTO 视图模型,可根据视图的特定需求进行定制。命令端不需要领域模型,因为视图可以直接从数据存储生成。查询端不需要在持久化存储上创建抽象,因此在这种情况下使用存储库毫无意义;这里应该使用持久化框架或像 ADO.NET 这样的轻量级库。
The architecture of the query side, as shown in Figure 24.4, is concerned with reporting on the domain. The objects returned from the query side are simple DTO view models tailored to the specific needs of the view. The domain model for the command side is not required, because a view can be generated directly from the data store. The query side does not need to create an abstraction over the persistence store, so a repository in this context makes no sense; a persistence framework or light-weight libraries like ADO.NET should be used here.
由于读取模型仍然位于领域层,因此如果数据存储中没有预先计算这些数据,它就可以使用读取端的领域对象来执行计算。通常,规范类被用来根据从数据库中提取的一些数据来提供对视图模型属性的响应。
Because the read model is still within the domain layer, it is able to use domain objects from the read side to perform calculations if this data is not precalculated within the data store. Typically, specification classes have been employed to provide an answer to view model properties based on some data pulled back from the database.
架构的读取端将建模为视图模型的报告请求直接映射到数据模型,完全绕过命令模型。可以在数据模型中为每个 UI 屏幕或报告构建视图。这样可以生成预先汇总的视图,这些视图检索速度快,操作简单将原始数据映射到视图模型时,能够处理分页、排序和自由文本搜索。
The read side of the architecture maps report requests modeled as view models directly to the data model, bypassing the command model completely. Views can be built within the data model for each UI screen or report. This results in presummarized views, which are fast to retrieve, simple when mapping the raw data to view models, and able to handle paging and sorting and free text searches.
如果命令端没有预先计算所需值,则使用规范或域服务来动态计算该值,如清单 24-5 所示。
If the command side does not pre-computer a required value, use a specification or domain service to calculate the value on the fly as shown in Listing 24-5.
您可以在查询服务中使用微型对象关系映射器 (ORM)——一种轻量级的程序,可以快速将原始数据映射到 DTO 视图模型。读取端将非常简单——除了拉取数据、映射到 DTO 以及委托规范或领域服务根据保存的状态进行决策之外,无需任何逻辑。
You can use a micro Object Relational Mapper (ORM) in the query service—something lightweight that can quickly map raw data to a DTO view model. The read side will be simple—devoid of any logic save that of pulling data and mapping to DTOs and delegating to specifications or domain services for decisions based on saved state.
您还可以通过使用不同的数据模式进一步实现读写分离。您可以根据命令引发的领域事件构建读取模型;然后,您可以使用这些事件构建物化视图,如图 24.6所示。
You can go further still with the segregation of read and write by using a different data schema. You can build a read model from domain events raised from commands; you can then use these events to build materialized views, as shown in Figure 24.6.
图 24.5 CQRS 的查询端。
FIGURE 24.5 The query side of CQRS.
图 24.6使用不同的数据存储进行查询。
FIGURE 24.6 Using a different data store for querying.
读取数据模型可以非规范化并针对查询进行优化,包括预先计算的数据。
A read data model can be denormalized and optimized for querying, including precalculated data.
清单 24-6 展示了命令模型上的操作如何引发领域事件,然后通过更新读取模型来持久化。这可以发生在同一事务和同一数据库中。稍后您将看到如何在进程外更新读取模型以使您的应用程序能够扩展。
Listing 24-6 shows how an action on the command model leads to a domain event being raised and then persisted by updating the read model. This can happen within the same transaction and the same database. Later you will see how updating the read model out of process can enable your application to scale.
关于 CQRS 模式存在许多误解,但正如您所读到的,它很简单,其核心是在特定上下文中使用特定模型。如果您在线阅读过任何关于 CQRS 的文章,您可能会认为您需要使用重量级消息传递框架,或者您的读取存储需要最终一致性。事实并非如此,尽管您稍后会读到,您可以使用这些技术来扩展 CQRS 模式,以便在需要时扩展您的应用程序。本节列出了您可能听说过的许多关于 CQRS 模式的常见误解。
There are many misconceptions about the pattern of CQRS, but as you have read, it is simple, and at its core is the case of using a specific model for a specific context. If you have read anything of CQRS online, you may be thinking that you need to use heavyweight messaging frameworks, or your read store needs to be eventually consistent. This is not the case, although as you will read later you can employ these techniques to extend the CQRS pattern to scale your application when required. This section lists many of the popular misconceptions of the CQRS pattern that you may have heard.
如果您从头开始阅读本章,您应该对 CQRS 感到熟悉,并且应该已经意识到它是一种简单的模式。从根本上讲,它是在领域模型层应用的单一职责原则 (SRP) 的实现。它有助于解决当展示模型与事务模型不一致时产生的复杂性。CQRS 并没有规定框架、多个数据库或设计模式。它只是指出应该分别处理这两种情况以提高效率。这是一种概念上的思维转变,而不是您需要采用的一系列复杂模式和原则。
If you have read this chapter from the beginning, you should be comfortable with CQRS, and you should have realized that it is a simple pattern. At a fundamental level, it’s an implementation of the Single Responsibility Principle (SRP) applied at the domain model layer. It’s useful for solving the complexity that arises when a presentational model is not in alignment with a transactional model. CQRS does not prescribe frameworks, multiple databases, or design patterns. It only states that the two contexts should be handled separately for better effectiveness. It’s a conceptual mind shift rather than a collection of complex patterns and principles that you need to adopt.
最终一致性是指读取模型在进程外更新,并与事务模型的更新异步进行的做法。这并非 CQRS 的先决条件,但它通常用于支持模型的读取端扩展。最终一致性读取模型会给应用程序增加额外的复杂性,因为用户在查看操作结果时可能会惊讶地看到过时的屏幕。CQRS 并不要求您实现最终一致性。您可以使用相同的数据库和事务来更新读取模型架构。事实上,如果您大量使用缓存,应用程序的读取存储可能已经实现了最终一致性。如果您由于复杂性导致呈现和事务关注点不一致而采用 CQRS 模式,请尝试从立即一致性开始,并且仅在遇到性能问题时才迁移到最终一致性。使用最终一致性读取存储会产生开销,您将在本章后面了解相关内容。
Eventual consistency is the practice of having a read model updated out of process and asynchronously to the update of the transactional model. This is not a prerequisite of CQRS, but it is often used to enable the read side of a model to scale. Eventually consistent read models add an extra layer of complexity to an application as users who check to see the result of their actions may be surprised to see an outdated screen. CQRS does not require you to be eventually consistent. You can use the same database and transaction to update the read model schema. In fact, your application’s read store may already be eventually consistent if you are heavily using caching. If you are adopting the CQRS pattern due to complexities resulting in the misalignment between presentation and transaction concerns, try starting off being immediately consistent, and only move to eventually consistent if you have performance issues. There is an overhead that you will learn about later in this chapter through utilizing eventually consistent read stores.
正如第23章所述,使用事件源是构建读取和写入模型的有效方法;然而,使用事件源或CQRS的领域事件并没有任何先决条件。事件源是确保审计跟踪准确性的解决方案,但它确实使构建读取模型变得更容易,因为您可以从历史事件数据中创建任何您想要的投影。
As covered in Chapter 23, using event sourcing is an effective method to build both the read and the write models; however, there is no prerequisite to using event sourcing or in fact domain events with CQRS. Event sourcing is a solution to a problem of ensuring that your audit trail is accurate, but it does make building the read model easier because you can create whatever projections you want from the historical event data.
CQRS 并不要求命令以“发射后不管”的方式发送。对于高度协作的领域,即多个用户对同一数据进行更改,异步命令是合理的。这使得它们能够被依次处理,并允许应用程序扩展,避免负载过重。然而,对于那些不返回成功或失败确认的命令,需要通过其他方式来告知用户操作是否成功。这可以是通过电子邮件或处理失败消息的额外行为。在采购的案例中,异步命令可能是为客户寻找替代品,而不是直接让订单失败。
CQRS does not insist on commands being sent in a fire-and-forget fashion. For highly collaborative domains, in which multiple users are making changes to the same data, asynchronous commands make sense. This enables them to be handled in turn and allows the application to scale and not be overwhelmed with load. However, commands that don’t return an acknowledgement regarding the success or failure require other ways to update the user to the success of an action. This could be via e-mail or extra behavior that handles failed messages. In the case of purchasing, this could be sourcing a substitution for the customer as opposed to simply failing an order outright.
如果您希望应用最终一致性读取存储或异步处理命令,那么使用消息传递框架可能是一个好主意。但是,如果您不打算这样做,那么在应用程序中添加消息传递系统只会增加不必要的复杂性。
If you are looking to apply an eventually consistent read store or process commands asynchronously, then using a messaging framework is probably a good idea. However, if you are not, then adding a messaging system to your application is just needless complexity.
使用事件构建物化读取模型是保持读取和写入模型分离的有效方法;然而,这并非必需,您可以使用其他方法来创建物化读取存储。正如您在第21章“存储库”中所见,聚合可以通过使用备忘录模式来揭示状态。您还可以使用第26章中的某些模式,以非阻塞的方式直接从域对象提供演示信息。最后,您可以基于写入模型的关系数据模型构建视图。
Using events to build a materialized read model is an effective method to keep your read and write models separate; however, it is not critical, and you can use other methods of creating a materialized read store. As you have seen in Chapter 21, “Repositories,” aggregates can reveal a state by using the memento pattern. You can also use some of the patterns in Chapter 26 to provide presentation information directly from your domain objects in a nonobstructive manner. Finally, you can build views based on the relational data model of the write model.
CQRS 使应用程序在高负载下也能表现良好。这是通过拆分应用程序的读写端来实现的。将读写端分离可以使每个端独立扩展,以满足应用程序的特定需求。读写数据也可以分离到最适合其需求的存储中。仅处理聚合的写入存储可以使用文档数据库或键值存储。读取模型可以使用关系数据库或缓存存储。
CQRS enables applications to perform well under heavy load. This is accomplished by the read and write side of an application being split. Separating the sides enables each to be scaled independently to meet the particular demands of the application. Read and write data can also be separated into stores that are best suited to their needs. A write store that deals solely with aggregates can utilize a document database or a key value store. A read model can utilize a relational database or a caching store.
然而,无论选择哪种方式进行扩展,都需要了解其中涉及的权衡。系统扩展不仅仅是一个技术决策。至关重要的是,企业必须理解,系统架构的变更会导致用户体验的变化,并且需要谨慎处理此类变更,并确保业务能够接受。第 11 章“有界上下文集成简介”中讨论的 CAP 定理指出,您可以同时拥有以下两项:一致性、可用性和分区容忍度。通过牺牲即时一致性并转向最终一致性更高的读取端,您可以扩展对系统报告要求较高的应用程序。对于高度协作的领域,您可以通过不立即处理请求来牺牲可用性保证。这涉及将发送到系统的消息排队并通过其他方式(例如电子邮件)在进程外处理,或者向客户端发送状态报告以指示请求成功。
However, to scale either side, you need to understand the trade-offs involved. Scaling out a system isn’t simply a technical decision. It is vital that the business understands that changes to the architecture of a system result in changes to the user experience, and such changes need to be handled carefully and be acceptable to the business. The CAP theorem discussed in Chapter 11, “Introduction to Bounded Context Integration,” states you can have two of the following: consistency, availability, and partition tolerance. By sacrificing immediate consistency and moving to a more eventually consistent read side, you are able to scale applications that have high demands on the report from the system. For domains that are highly collaborative, you can sacrifice the availability guarantee, by not handling a request immediately. This involves messages sent to the system being queued and handled out of process by other means, such as e-mail, or a status report signifying to the client the success of the request.
最重要的一点是,任何为了扩展应用程序而做出的权衡都会影响用户体验,进而影响您的业务。因此,业务决策必须能够影响用户体验。本节将探讨如何使用 CQRS 模式同时扩展读取端和写入端,以及您需要考虑的权衡因素。
The most important point to take way is that any trade-offs made to scale your application affect user experience, and this affects your business. Therefore, it is important that the business make decisions that affect user experience. This section examines ways to scale both the read and the write side using the CQRS pattern, along with the trade-offs that you need to consider.
如果您的应用程序在读取端的需求远大于写入端,那么拥有最终一致的读取存储可以提高应用程序的可用性和性能。您可以将读取模型存储在与写入端不同的数据库中,也可以将其复制到多个数据库或任何持久性存储中。在读取端存储数据的方式可能与在写入端存储领域状态的方式完全不同。但是,要做到这一点,您必须将状态更改从写入端发布到读取端,以便对其进行非规范化并专门存储以用于查询检索。图 24.7显示了架构的读取端和写入端之间存在一个队列。该队列包含一个领域事件,该事件在领域模型内的状态更改时触发。读取端处理该领域事件并更新其读取存储。随着读取端的增长与写入端略微不同步,读取端将变得最终一致。
If the demands of your application are far greater on the read side than on the write side, having an eventually consistent read store can allow you to increase the availability and performance of your application. You can store a read model in a separate database from your write side, or you can replicate it to multiple databases or any persistence store. The way you store your data on the read side can be completely different from the way you store the state of your domain on the write side. However, to do so you must publish state changes from the write side to the read side so that they can be denormalized and stored specifically for query retrieval. Figure 24.7 shows that there is a queue between the read and the write side of the architecture. This queue contains a domain event that is raised upon a state change within the domain model. The read side processes that domain event and updates its read store. Your read side becomes eventually consistent as it grows slightly out of sync with the write side.
图 24.7最终一致的读取模型。
FIGURE 24.7 An eventually-consistent read model.
最终一致的读取存储会影响您的用户体验。您的报告数据(UI 显示和传统报告)需要保持多高的更新度,对于您的业务用户来说是一个问题。有必要明确说明数据的过时程度,以便用户在根据他们面前的数据做出决策时能够考虑到这一点。
An eventually consistent read store affects your user experience. How up-to-date your reporting data (UI display and traditional reporting) needs to be is a question for your business users. It is worth being explicit about the staleness of the data so users can consider it when making decisions on the data they have in front of them.
您可以使用读取模型来整合整个企业的视图,以简化报表渲染。其他通过 Restful URL 或消息传递系统公开其域状态报表的有界上下文,可以在读取存储中使用,以整合满足复合 UI 所需的数据。图 24.8展示了这样一个系统的外观。
You can use a read model to consolidate views from across your enterprise to simplify report rendering. Other bounded contexts that expose reports on the state of their domains via Restful URLs or messaging systems can be consumed and used within the read store to consolidate the data required to satisfy a composite UI. Figure 24.8 shows how such a system might look.
图 24.8将来自多个有界上下文的数据整合到单个读取模型中。
FIGURE 24.8 Consolidate data from many bounded contexts into a single read model.
您可能已经将事务数据库的副本用作报表数据库,并通过日志传送进行复制。这是一种分离读写操作的方式。如果适用,您可以使用这种简单的方法来扩展读取端,如图24.9所示。
You may already be using a copy of your transactional database as a reporting database that is replicated through log shipping. This is a form of separating your read and write concerns. If it is applicable, you can use this simple method to scale your read side, as shown in Figure 24.9.
图 24.9使用事务数据库的副本作为读取模型。
FIGURE 24.9 Use a copy of the transactional database for the read model.
如果您拥有一个高度协作的领域,其中许多用户对同一组实体进行更改,那么在进程外处理业务任务可以让您扩展应用程序以处理高负载。图 24.10展示了如何使用消息队列存储业务任务请求。应用层接受来自客户端的执行业务任务的请求,但它不会立即执行该请求,而是在进程外处理该请求。应用层只能确认请求已收到;必须通过电子邮件或简单地检查请求状态等其他方式通知客户端。
If you have a highly collaborative domain with many users making changes to the same set of entities, handling business tasks out of process enables you to scale your application to handle the high load. Figure 24.10 shoes how you can use a message queue to store requests for business tasks. The application layer accepts the request from a client to perform a business task, but instead of executing the request straight away, it handles it out of process. The application layer can only acknowledge that the request was received; the client must be notified in a different manner through an e-mail or simply by checking on the status of the request.
图 24.10异步写入端。
FIGURE 24.10 An asynchronous write side.
如果您异步发送业务任务请求,则需要尽可能确保请求能够成功,因为您无法立即收到请求成功或失败的确认。应用服务应该对请求执行一些基本验证,甚至可能在将请求添加到队列以供稍后处理之前使用视图存储来检查不变量。应用程序应该进行足够的验证工作,以确保如果请求失败,是由于业务原因,而不是由于参数缺失或错误。
If you are sending requests for business tasks asynchronously, you need to be sure, as far as you can, that they will succeed because you have no way of receiving immediate acknowledgement to the success or failure of the request. The application service should perform some basic validation of the request and perhaps even use the view store to check invariants before adding the request to the queue for processing later. An application should do enough validation work to ensure that if a request fails, it is because of business reasons and not because of a missing or incorrect parameter.
为了尽量减少用户的困惑,请明确说明请求的执行是在流程外处理的,确认仅确认请求已被接受,而不是已成功。在某些领域,这种体验(例如在电子商务网站上订购)对用户来说是正常的。只要用户收到订单请求已下达的确认并且他有订单 ID,他就会很高兴。他还知道,在处理订单时,他的卡可能会失败,或者订购时有库存的某些商品现在可能缺货并需要补货。但是,如果您所在的领域用户希望查看他们的请求旨在进行的更改,则需要明确说明将会有延迟。
To minimize confusion for users, make it clear that the execution of a request is handled out of process, and an acknowledgement merely confirms that a request was accepted, not that it has succeeded. In some domains, this kind of experience, such as placing an order on an e-commerce site, is normal to a user. As long as the user receives confirmation that a request for an order has been placed and that he has an order ID, he is happy. He is also aware that when it comes to processing the order, his card may fail or some items that were in stock at the time of ordering may now be out of stock and on order for replenishment. If, however, you are in a domain in which users expect to view the changes that their request was intended to make, you need to make it explicit that there will be a delay.
如果您在读写操作繁重的协作领域工作,则可以结合进程外处理的业务任务使用最终一致性读取模型。图 24.11展示了如果需要扩展应用程序的读写端,两端的视图将会如何。
If you are working in a collaborative domain with heavy reads and writes, you can use an eventually consistent read model in conjunction with business tasks being handled out of process. Figure 24.11 shows you how both sides would look if you needed to scale out the read and write sides of an application.
图 24.11扩展 CQRS 的读写端。
FIGURE 24.11 Scaling out the read and write sides of CQRS.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 25 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 25 download and individually named according to the names throughout the chapter.
领域驱动设计 (DDD) 的诸多优势源于对项目通用语言 (UL) 的规范使用——无论是在对话中还是在代码中。然而,您面临的一大挑战是如何在代码中保持领域概念的明确性,同时又要努力将领域概念与纯技术问题区分开来。例如,当您与领域专家深入探讨您的领域模型时,最好不要让线程、套接字或数据库连接扰乱您的思路或对话。因此,为了最大限度地提高领域模型的明确性,务必将现实世界的领域概念与纯技术问题明确区分。分离是应用服务所承担的重要角色之一,应用服务属于应用服务层。
Many of Domain-Driven Design (DDD’s) benefits arise from disciplined use of a project’s ubiquitous language (UL)—both in conversation and in code. One of the big challenges you face, though, is maintaining explicitness of domain concepts in code as you try to keep them isolated from purely technical concerns. For instance, when you are knowledge-crunching with domain experts, talking them through your domain model, it’s not ideal to clutter your thinking—or the conversation—with threads, sockets, or database connections. Therefore, to maximize the explicitness of your domain model, a clear separation between real-world domain concepts and purely technical concerns is highly desirable. This separation is one of the important roles carried out by application services, which belong to the application service layer.
从逻辑上讲,应用服务层位于领域之上,并依赖于领域。这意味着应用服务的一项关键职责是与领域协调,以执行完整的业务用例。作为这项职责的一部分,应用服务必须转换输入和输出以保护领域结构,并且通常需要使用 REST、消息传递以及第二部分“战略模式:有界上下文之间的通信”中讨论的其他概念与其他有界上下文进行通信。图 25.1高度概括地展示了应用服务的角色。
Logically, the application service layer sits above the domain and is dependent upon it. This means that a crucial responsibility of application services is to coordinate with the domain to carry out full business use cases. As part of this responsibility, an application service has to translate inputs and outputs to protect domain structure, and it often needs to communicate with other bounded contexts using REST, messaging, and other concepts discussed in Part II, “Strategic Patterns: Communicating Between Bounded Contexts.” Figure 25.1 provides a high-level illustration of the role of application services.
图 25.1应用服务适用的地方。
FIGURE 25.1 Where application services fit in.
图 25.1通过可视化的方式展示了应用服务如何在领域和外部服务(例如其他有界上下文)之间进行协调,从而设定了本章的上下文。在继续介绍演示应用服务职责的技术示例之前,务必先明确应用逻辑和领域逻辑之间的区别。即使是经验丰富的 DDD 从业者,有时也会对这种区别感到困惑。
Figure 25.1 sets the context for this chapter by visualizing how application services mediate between the domain and external services such as other bounded contexts. Before moving on to technical examples that demonstrate the responsibilities of application services, it is important to be clear about the distinction between application and domain logic. Even for experienced DDD practitioners, there are occasionally some challenges with this distinction.
如果您想创建一个能够突出领域概念并将其与纯技术细节区分开来的模型,那么理解应用逻辑和领域逻辑之间的区别至关重要。尽管存在一些模糊地带,但在大多数情况下,这并不是一项艰巨的任务。本节旨在提供一个清晰的框架,让您无需担心代码应该放在何处。
Understanding the difference between application logic and domain logic is crucial if you want to produce a model that accentuates domain concepts and isolates them from purely technical details. For the most part, it is not a difficult task, although there is a small gray area. This section aims to provide a clear picture so you rarely have to worry about where your code should live.
首先,您可以设想应用服务具有两项常规职责。首先,它们负责基础设施:管理事务、发送电子邮件以及类似的技术任务。此外,应用服务必须与领域协调以执行完整的业务用例。正确履行这些职责有助于防止领域逻辑在应用服务中被混淆或错误定位。
As a starting point, you can think of application services as having two general responsibilities. First, they are responsible for infrastructural concerns: managing transactions, sending e-mails, and similar technical tasks. In addition, application services have to coordinate with the domain to carry out full business use cases. Carrying out these responsibilities correctly helps prevent domain logic from being obfuscated or incorrectly located in application services.
为了演示应用服务的作用,本节将演示如何创建一个可用于在线赌博应用程序的应用服务。该服务管理“推荐朋友”用例,其中,如果忠实的赌徒能够推荐其朋友注册,他们将获得 50 美元的免费积分奖励。他们的朋友在创建账户时也会获得 50 美元的积分奖励。作为额外奖励,企业决定将推荐人的忠诚度等级升级为黄金级。图 25.2展示了“推荐朋友”用例。
To demonstrate the role of application services, this section walks through the creation of an application service that can be used in an online gambling application. It manages the Recommend-a-Friend use case, in which loyal gamblers are rewarded with $50 of free credit if they can entice their friends to sign up. Their friends are also rewarded with a $50 credit when their account is created. As an extra bonus, the business has decided the referrer’s loyalty status should be upgraded to gold. Figure 25.2 illustrates the Recommend-a-Friend use case.
图 25.2推荐朋友用例。
FIGURE 25.2 The Recommend-a-Friend use case.
要调用领域模型的功能,您需要执行一系列基础任务。设置数据库连接就是一个常见的例子。如果您能够很好地隔离这些基础代码,就能获得一个更易于维护、摆脱技术障碍的领域模型。在接下来的几个简短章节中,您将看到在RecommendAFriendService中处理基础架构问题的示例。RecommendedAFriendService 是一个应用服务,负责在在线赌博场景中执行“推荐朋友”用例。与许多其他应用服务一样,该应用服务首先要处理的任务之一是验证输入。
To invoke the functionality of your domain model, you need to carry out a number of infrastructural tasks. Setting up database connections is one common example. When you do a good job of isolating this infrastructural code, you’re rewarded with a more maintainable domain model, free from technical clutter. In the following short sections, you see examples of infrastructural concerns being handled in the RecommendAFriendService—an application service that is responsible for carrying out the Recommend-a-Friend use case in the online gambling scenario. One of the first tasks this application service has to deal with, like many others, is validating inputs.
应用程序级验证的示例包括检查参数的数据类型、格式和长度是否正确。这些并非领域专家关心的业务规则,但仍可能导致系统出现错误。与其让这些技术细节扰乱领域逻辑,不如在应用服务中执行此类验证。
Examples of application-level validation include checking that parameters are the correct data type, the correct format, and the correct length. They aren’t business rules that domain experts care about, but they can still cause error conditions in a system. Rather than cluttering domain logic with these technical details, you can perform this type of validation in application services.
RecommendAFriendService接收推荐人的账户 ID 和注册好友的基本账户信息。RecommendedAFriendService 执行应用程序级验证的初始实现如清单25-1 所示。
The RecommendAFriendService takes the account ID of the referrer and basic account details of the friend who is signing up. An initial implementation of the RecommendAFriendService carrying out application-level validation is shown in Listing 25-1.
清单 25-1 示例了应用程序级验证的作用。Validate ()中的每个子句都会检查一个技术细节,例如在电子邮件中检查字符串长度或字符串格式。它们都不是业务规则,也不属于领域规则。
LISTING 25-1 exemplifies the role of application-level validation. Each clause in Validate() checks a technical detail such as string length or string format in the case of an e-mail. None of them are a business rule, and none of them belong in the domain.
在典型的业务用例中,通常有多个操作需要在事务内同时成功或失败。通过管理应用服务中的事务,您可以完全控制哪些域请求的操作将存在于同一事务边界内。这可以通过更新的RecommendAFriendService来演示。假设企业决定,如果无法应用推荐策略,则不应创建新帐户。因此,事务边界封装了创建新帐户并将推荐策略应用于两个帐户的操作,如图 25.3所示。
In a typical business use case there are often multiple actions that need to succeed or fail together inside a transaction. By managing transactions in application services, you have full control over which operations that you request of the domain will live inside the same transaction boundary. This can be demonstrated using an updated RecommendAFriendService. Imagine the business has decided that if the referral policy cannot be applied, it should not create the new account. Therefore, the transactional boundary encapsulates creating the new account and applying the referral policy to both accounts, as shown in Figure 25.3.
图 25.3推荐朋友的事务边界。
FIGURE 25.3 Transactional boundary for Recommend-a-Friend.
使用 .NET Framework 的TransactionScope ,您可以向RecommendAFriendService添加事务行为,如清单 25-2 所示。并非所有数据库、ORM 和其他框架都使用TransactionScope,但用于创建、提交和中止事务的应用程序编程接口(API)通常是相似的。
Using the .NET Framework’s TransactionScope, you can add transactional behavior to the RecommendAFriendService, as shown in Listing 25-2. Not all databases, ORMs, and other frameworks use TransactionScope, but application programming interfaces (APIs) for creating, committing, and aborting transactions are usually similar.
并非所有与域的交互都会成功。在这种情况下,域可能会抛出异常或返回错误代码。这些情况表明域验证失败(即使应用程序级验证成功)。应用服务的工作是处理这些错误情况,并将其转换为适合外部方的表示形式,以便任何外部方都不会与域错误的结构耦合。外部方可以是网站的人类用户,也可以是其他软件系统。
Not all interactions with the domain will be successful. In such cases, the domain will likely throw exceptions or return error codes. These are cases in which domain validation fails (even though application-level validation was successful). An application service’s job is to handle these error conditions and translate them into suitable representations for external parties, so that no external parties are coupled to the structure of the domain errors. External parties could be either human users of a website or other software systems.
在“推荐朋友”用例中,可能出现的一个域错误是,推荐人有长期未结余额。企业决定,这些客户不应该获得忠诚度奖励。由于领域将通知此错误情况,因此RecommendAFriendService负责处理错误并将其转换为合适的外部表示,如清单 25-3 所示。
One domain error that can occur in the Recommend-a-Friend use case is when the referrer has a long-term outstanding balance. The business has decided that these customers should not be rewarded with a loyalty bonus. Because the domain will notify of this error condition, the RecommendAFriendService is responsible for handling the error and transforming it into a suitable external representation, as shown in Listing 25-3.
清单 25-3 展示了应用服务如何通过将异常转换为标准格式来保护领域结构。在此示例中,外部格式是ApplicationError异常。使用ApplicationError异常可以防止应用服务的客户端与领域异常耦合。相反,客户端与ApplicationError耦合,后者是一个更稳定的接口,可以隐藏领域的潜在波动性。
Listing 25-3 shows how application services can protect the domain structure by translating exceptions into a standard format. In this example, the external format is an ApplicationError exception. Using an ApplicationError exception prevents clients of the application service becoming coupled to the domain exception. Instead, clients are coupled to the ApplicationError, which is a more stable interface hiding the potential volatility of the domain.
使用约定来表示错误(例如ApplicationError异常),使您有机会一致地处理所有错误。例如,在 ASP.NET MVC 应用程序中,您可以创建一个HandleErrorAttribute来处理ApplicationError。当过滤器捕获到ApplicationError时,它知道它是由应用程序服务抛出的,这意味着可以安全地将消息呈现给外界。相反,当它捕获到任何其他类型的异常时,它不知道披露错误的详细信息是否安全,因此它必须返回一个通用的错误消息,如清单25-4所示。
Using a convention for representing errors, such as an ApplicationError exception, gives you the opportunity to handle all errors consistently. For example, in an ASP.NET MVC application, you can create a HandleErrorAttribute that handles ApplicationErrors. When the filter catches an ApplicationError, it knows that it was thrown by an application service, meaning that it is safe to present the message to the outside world. In contrast, when it catches any other type of exception, it does not know if it is secure to disclose the details of the error, so it has to return a generic error message, as shown in Listing 25-4.
响应时间、错误和其他类型的诊断信息可以帮助您了解应用程序的运行情况,并及早发现任何潜在问题。但是,捕获这些信息可能会给您的域逻辑带来不必要的混乱,并使重要的概念变得模糊。有时您不得不忍受这种痛苦,但在很多情况下,您可以依靠应用服务来报告这些信息。
Response times, errors, and other types of diagnostic information allow you to see how your application is performing and spot any potential issues at an early stage. But capturing this information can add unnecessary clutter to your domain logic and obfuscate important concepts. Sometimes you have to suffer this pain, but on many occasions, you can lean on application services to report this information instead.
清单 25-5 显示了RecommendAFriendService的更新版本,它记录每个操作的持续时间以及总体结果是异常还是成功,这样就不必将代码添加到域模型中。
Listing 25-5 shows an updated version of the RecommendAFriendService that logs the duration of each action and whether the overall result is an exception or a success so that the code does not have to be added to the domain model.
清单 25-5 中需要注意的关键细节是logger.Debug()、logger.Error()和StatsDClient.Metrics()的用法。这些方法调用很容易与领域逻辑混合在一起。然而,它们都被引入到了应用服务中。
The key details to look for in Listing 25-5 are the usages of logger.Debug(), logger.Error(), and StatsDClient.Metrics(). Each of these method calls could have easily ended up mixed in with domain logic. Instead, each has been brought up into the application service.
大多数应用程序被迫处理的一个常见基础设施问题是身份验证。为了演示推荐好友场景中的身份验证,此示例模拟了客户支持登录管理界面并手动触发推荐策略的情况,因为新客户注册时出现问题。清单 25-6 展示了AdminRecommendAFriendService如何在应用推荐策略之前检查用户是否已通过身份验证。
A common infrastructural concern that most applications are forced to deal with is authentication. To demonstrate authentication in the Recommend-a-Friend scenario, this example models the case in which customer support is logging in to the admin interface and manually triggering the referral policy because there was a problem when the new customer signed up. Listing 25-6 shows how the AdminRecommendAFriendService checks whether the user is authenticated before it applies the referral policy.
对于许多应用程序来说,身份验证并非足够强大的安全措施,还需要授权。授权是检查用户是否拥有执行请求操作的适当权限的过程。因此,对于拥有不同类型用户和不同权限的应用程序中,授权是一个重要的工具。
For many applications, authentication isn’t a strong enough security measure, and authorization is additionally required. Authorization is the process that checks whether users have the appropriate privileges to carry out the requested action. Therefore, it’s an important tool in applications that have different types of users with different privileges.
假设AdminRecommendAFriendService是一个应用程序的一部分,该应用程序包含许多不同的用户(客户和管理员),因此需要授权以防止普通用户应用推荐策略;如果用户只需破解几个统一资源定位符(URL)就能不断地用免费积分充值他们的在线赌博账户,那么业务就会受到负面影响。(这种情况确实会发生。)清单 25-7 显示了执行管理员授权检查的AdminRecommendAFriendService的升级版本。
Assuming that the AdminRecommendAFriendService is part of an application that contains a number of different users—customers and admins—authorization will be required to prevent normal users from applying the referral policy; business would be negatively affected if users could continually top up their online gambling account with free credits by just hacking a few uniform resource locators (URLs). (This does happen.) An upgraded version of the AdminRecommendAFriendService that performs an admin authorization check is shown in Listing 25-7.
发生在领域内部、来自一个有界上下文的事件可能会触发由其他有界上下文处理的事件。您在第二部分中看到了消息传递和 REST 示例。应用服务负责在有界上下文之间传输事件。这可能涉及使用消息总线(详细示例参见第 12 章“通过消息传递集成”)或原子 feed(详细示例参见第 13 章“通过 HTTP 与 RPC 和 REST 集成”)或其他类型的通信发布消息。同样重要的是,目标是将领域概念与基础设施问题解耦。清单 25-8 展示了RecommendAFriendService的更新版本,它也使用 NServiceBus 发布事件。
Events that happen inside a domain from one bounded context may trigger events that are handled by other bounded contexts. You saw examples of this in Part II with the messaging and REST examples. It is the responsibility of an application service to transmit events between bounded contexts. This may involve publishing messages using a message bus (see Chapter 12, “Integrating Via Messaging,” for detailed examples) or an atom feed (see Chapter 13, “Integrating Via HTTP with RPC and REST,” for detailed examples), or other kinds of communication. Importantly, again, the goal is to decouple domain concepts from infrastructural concerns. Listing 25-8 shows an updated version of the RecommendAFriendService that also publishes events using NServiceBus.
虽然清单 25-8 展示了一个通过 NServiceBus 进行通信的应用服务,但通信方法可以是远程过程调用 (RPC)、REST 或其他。应用服务还应该用于处理本书第二部分中提到的其他问题,包括处理外部事件、处理和发布内部事件(例如第 12 章中的消息传递网关)以及生成原子提要。
Although Listing 25-8 shows an application service communicating via NServiceBus, the method of communication could be a remote procedure call (RPC), REST, or another. Application services should also be used to handle other concerns presented in Part II of the book, including handling external events, handling and publishing internal events (such as the messaging gateway in Chapter 12), and producing an atom feed.
回顾清单 25-8 中RecommendAFriendService的最新版本,可以看到它处理了许多基础设施关注点,而这些关注点本质上并不增加业务价值。它们只是为了支持应用服务的第二个主要职责,即协调领域模型来执行完整的业务用例,从而增加价值。清单 25-9 展示了RecommendAFriendService更新版本的一部分,它建立在基础设施基础上,通过协调领域模型来执行“推荐朋友”用例。
Looking back to the latest version of the RecommendAFriendService in Listing 25-8, you can see it handles a lot of infrastructural concerns that do not intrinsically add business value. They are only there to support the second major responsibility of application services that does add value—coordinating the domain model to carry out full business use cases. Listing 25-9 shows a section of an updated version of the RecommendAFriendService that builds on the infrastructural foundations by coordinating the domain to carry out the Recommend-a-Friend use case.
您可以在清单 25-9 中看到customerDirectory和RecommendAFriendPolicy变量的使用。它们代表域对象,并在本例中用于举例说明应用服务如何与域进行多次交互以执行整个用例。在这里,它获取引用者,要求域创建新客户,并最终告诉域应用该策略。
You can see the customerDirectory and RecommendAFriendPolicy variables being used in Listing 25-9. They represent domain objects and are used in this example to exemplify how an application service has to interact multiple times with the domain to carry out the entire use case. Here, it fetches the referrer, asks the domain to create the new customer, and finally tells the domain to apply the policy.
应用服务因充斥着松散关联的逻辑而变得臃肿,这些逻辑似乎不属于其他任何地方。有时,可以将代码块重构到它们自己的类中。在RecommendAFriendService中,Validate()非常适合移到其自己的类中。应用服务中发现的另一个臃肿来源是样板代码重复。在RecommendAFriendService中,事务代码是典型的样板代码,最终可能会在每个应用服务中重复出现。您可以创建一个遵循模板方法模式的基础应用服务,也可以创建一个接受 lambda 表达式并用事务包装它的实用程序。
Application services are notorious for becoming bloated with loosely related logic that doesn’t appear to belong anywhere else. Sometimes chunks can be refactored into their own classes. In the RecommendAFriendService, Validate() is a good candidate for moving into its own class. Another source of bloat found in application services is boilerplate duplication. In the RecommendAFriendService, the transaction code is typical boilerplate code that could end up being duplicated in every application service. You could create a base application service that follows the Template Method pattern, or you could create a utility that takes a lambda and wraps it with a transaction.
即使使用了刚才提到的模式,有时也存在比使用包罗万象的应用服务更简洁的解决方案。许多现代框架都提供了钩子,供您注入基础设施关注点。ASP.NET MVC 就是一个很好的例子。它有ActionFilter的概念,它的作用类似于管道。使用动作过滤器,您可以注入处理验证、打开和关闭事务、日志记录以及其他基础设施问题的过滤器。清单 25-10 展示了一个TransactionFilter ActionFilter 的示例。
Even with the patterns just mentioned, there is sometimes a cleaner solution than using all-encompassing application services. Many modern frameworks provide hooks for you to inject your infrastructural concerns. ASP.NET MVC is a good example. It has the concept of ActionFilters, which act like a pipeline. Using action filters, you can inject filters that handle validation, opening and closing transactions, logging, and other infrastructural concerns. Listing 25-10 shows an example TransactionFilter ActionFilter.
当框架处理了所有基础设施问题时,是否需要应用服务是有争议的。相反,您可能会倾向于将几行领域模型协调代码直接放在控制器中,如清单 25-11 所示。
When the framework takes care of all your infrastructural concerns, the need for an application service is debatable. Instead, you may be tempted to put your few lines of domain model coordination directly inside a controller, as Listing 25-11 does.
尽管框架很诱人,但有一个很大的陷阱需要注意。如果您在多个地方(例如 Web 前端和桌面前端)使用应用服务,那么使用一个包罗万象的应用服务或许可以避免在两个前端重复构建基础架构逻辑。您需要根据每个项目的具体情况,决定与所用框架的耦合程度。
As enticing as frameworks are, there is a big gotcha to be aware of. If you are using the application service in multiple places—perhaps a web front end and a desktop front end—then using an all-encompassing application service may prevent you from having to duplicate the infrastructural logic in both front ends. You need to make a per-project decision about how tightly you want to couple yourself to the frameworks you are using.
对于许多 DDD 从业者来说,一项具有挑战性的工作是在应用逻辑和领域逻辑之间划清界限。基础设施关注点通常很容易识别,但拼凑完整用例的协调逻辑有时似乎与“应用服务中没有业务规则”的原则相矛盾。如果回顾清单 25-9,可以看到与领域逻辑的三种交互:获取引用者、请求领域逻辑创建新客户以及应用策略。有些人认为这种逻辑应该存在于领域逻辑中。
A challenging activity for many DDD practitioners is drawing the line between application logic and domain logic. Infrastructural concerns are normally easy to identify, but the coordination logic that pieces together full use cases can sometimes appear to contradict the “No business rules in application services” principle. If you look back to Listing 25-9, you can see three interactions with the domain: fetching the referrer, asking the domain to create the new customer, and applying the policy. Some would argue that this logic should live in the domain.
判断一系列交互是否属于领域概念的一种方法是问:“这应该总是发生吗?”或者“这些步骤是否密不可分?”如果是,这听起来像是领域策略,因为这些步骤必须始终同时发生。但是,如果这些步骤可以通过多种方式重新组合,则可能不是领域概念。在“推荐朋友”用例中,您可能会认为推荐策略应该只应用于现有客户(推荐人)和新客户(推荐人的朋友)。因此,应用服务中的逻辑实际上是一条业务规则,最好将其封装在领域服务中。但是,有时企业希望在创建过程中出现错误时将策略应用于两个现有帐户。在这种情况下,新帐户的创建和策略的耦合度较低,这表明这是应用逻辑。
One approach to deciding if a sequence of interactions belongs in the domain is to ask, “Should this always happen?” or “Are these steps inextricable?” If so, that sounds like a domain policy, because those steps always have to happen together. However, if those steps can be recombined in a number of ways, potentially it’s not a domain concept. In the Recommend-a-Friend use case, you might argue that the referral policy should only ever be applied to an existing customer (the referrer) and a new customer (the referrer’s friend). It follows, then, that the logic in the application service is actually a business rule that may be best encapsulated inside a domain service. However, sometimes the business wants to apply the policy to two existing accounts if there was an error during creation. In that case, the creation of the new account and the policy aren’t as coupled, suggesting this is application logic.
避免将领域概念泄露到应用服务的一个标志是,与领域的每次交互都富有表现力。例如,RecommendAFriendPolicy.Apply()。该方法的实现为每个账户存入 50 美元,并将推荐人提升至金牌会员等级。如果这些步骤都放在应用服务中,那么表达力就会不足,代码会更加冗长,而且这显然表明领域概念已经泄露到领域之外。
A sign that you have avoided leaking domain concepts into application services is that each interaction with the domain is expressive. An example of this is RecommendAFriendPolicy.Apply(). The implementation of this method credits each account with $50 and promotes the referrer to gold loyalty status. If those steps lived in the application service, it would be less expressive, more verbose, and a big sign that domain concepts had leaked out of the domain.
您可以根据自己的偏好和具体情况,在应用服务中使用各种设计模式和原则。在上一节的RecommendAFriendService中,您看到了原生的、类似脚本的面向对象代码。在很多情况下,这是最简单的代码。解决方案,也是一个不错的选择。在其他情况下,您可能会发现它会带来维护难题或不良的耦合。社区中出现了以下模式来解决此类问题。
You can use a variety of design patterns and principles inside application services based on your preferences and based on context. In the RecommendAFriendService from the previous section, you saw vanilla, script-like, object-oriented code. In many cases, that is the simplest possible solution and a good choice. On other occasions, you may find it presents maintenance headaches or undesirable coupling. The following patterns have arisen within the community for addressing concerns like these.
使用命令处理器模式,您可以避免开发存在诸多问题的大型应用服务。相反,每个用例都有一个命令和一个处理器。清单 25-12 展示了一个可被视为具有多项职责的单体应用服务。
Using the command processor pattern, you can avoid developing large application services with many concerns. Instead, you have a command and a processor for each use case. Listing 25-12 shows a monolithic application service that can be considered to have multiple responsibilities.
当您拥有像清单 25-12 中的BloatedRecommendAFriendService这样的应用服务时,可能会出现摩擦。每个方法的实现可能有很大差异,每个方法使用不同的依赖项。低内聚在使用 CQRS 的应用程序中尤其常见。因此,应用服务可能会成为开发摩擦的根源。命令处理器模式可以用来缓解这些痛苦。
Friction can occur when you have application services like the BloatedRecommendAFriendService in Listing 25-12. The implementations for each method may vary considerably, with each using different dependencies. Low cohesion is especially common in applications that use CQRS. Consequently, the application service can grow into a source of development friction. The command processor pattern can be used to alleviate these pains.
要使用命令处理器模式创建BloatedRecommendAFriendService的替代版本,第一步是创建一个表达意图并包含执行该意图所需的所有相关信息的命令。您可以在清单 25-13 中看到一个示例。
To create an alternative version of the BloatedRecommendAFriendService using the command processor pattern, the first step is to create a command that expresses intent and contains all the relevant information needed for it to be carried out. You can see an example of this in Listing 25-13.
创建命令后,接下来创建一个命令处理器。处理RecommendAFriend命令的命令处理器接口如清单 25-14 所示。
After creating the command, you then create a command processor. An interface for a command processor that processes RecommendAFriend commands is shown in Listing 25-14.
如上所述,创建命令和处理器本质上只是为应用程序中的每个用例创建单独的接口,目的是隔离职责并提高表达能力。但是,许多 DDD 从业者通过添加一个间接层来进一步发展该模式,该间接层提供了更松散的耦合和链接命令处理器的能力。链接可用于创建执行基础架构关注点(例如验证、事务和日志记录)的管道,从而允许您隔离域协调。此版本的模式需要通用处理器接口,如清单 25-15 所示。
Creating a command and processor as above is fundamentally just a case of creating a separate interface for each use case in the application, with the aim of isolating responsibilities and increasing expressiveness. But many DDD practitioners have taken the pattern further by adding a layer of indirection that provides looser coupling and the ability to chain command processors. Chaining can be used to create a pipeline that carries out infrastructural concerns such as validation, transactions, and logging, allowing you to isolate domain coordination. This version of the pattern requires common processor interfaces, as shown in Listing 25-15.
然后,每个命令处理器都实现此接口,以便它们可以链接在一起。首先,您需要创建一个特定于您正在实现的用例的处理程序。在本例中,它将是一个RecommendAFriendProcessor,如清单 25-16 所示。
Each command processor then implements this interface so they can be chained together. First, you want to create a handler that is specific to the use case you are implementing. In this case, that would be a RecommendAFriendProcessor, as shown in Listing 25-16.
可以应用于任何用例的通用命令处理器也需要实现ICommandProcessor接口。您可以在清单 25-17 中看到示例日志和事务处理器。
Generic command processors that you can apply to any use case also need to implement the ICommandProcessor interface. You can see demonstrative logging and transaction processors in Listing 25-17.
当你查看清单 25-17 时,需要注意的最重要的细节是每个命令处理器在其构造函数中都接受另一个处理器。当处理器处理命令时,它会调用传入其构造函数的处理器。本质上,它包装(或装饰)了“子”处理器,以创建一个可以无限长的管道。另一个需要注意的重要细节发生在Process()内部。处理器可以在其执行的处理器之前或之后执行逻辑。包装。正如您在TransactionProcessor中看到的,它启动一个事务,调用子进程(该子进程可能会调用其他子进程),然后根据包装的处理器是否引发异常来提交或回滚事务。
When you’re looking at Listing 25-17, the most important detail to discern is that each command processor takes another processor in its constructor. When a processor processes a command, it invokes the processor passed into its constructor. Essentially, it is wrapping (or decorating) the “child” processor to create a pipeline that can be infinitely long. Another important detail to be aware of occurs inside Process(). A processor can perform logic before and after the processor it wraps. As you can see with the TransactionProcessor, it starts a transaction, invokes the child (which may invoke other children), and then commits or rolls back the transaction depending on whether the wrapped processor threw an exception.
连接命令处理器是最后剩下的细节。你可以在代码清单 25-18 中看到最简单的版本。如果将代码与图 25-17 进行比较,应该能帮助你理解管道是如何创建的。
Wiring up command processors is the last remaining detail. You can see the simplest possible version of this in Listing 25-18. If you compare the code with Figure 25-17, it should help you see how the pipeline is being created.
除了清单 25-18 所示之外,还有其他连接管道的方法。有些团队确实更喜欢使用一些辅助方法以类似的方式手动连接每个管道,以避免重复。其他团队则更喜欢使用依赖注入。如何配置管道完全由您决定。
There are other ways of wiring up pipelines than that shown in Listing 25-18. Some teams do prefer to manually wire up each pipeline in a similar fashion to this using a few helper methods to avoid duplication. Other teams prefer to use dependency injection. It’s completely up to you to decide how to configure your pipelines.
松散耦合的一种模式是发布/订阅,即应用服务订阅域中的事件。当您的域逻辑本质上是基于事件的,尤其是当您将命令传递到域但不接收返回值时,您可能需要考虑这种模式。基于事件的ReferralPolicy版本的接口如清单 25-19 所示。
A pattern for looser coupling is publish/subscribe, whereby application services subscribe to events in the domain. You may want to consider this pattern when your domain logic is inherently event based, especially when you pass commands into the domain but do not receive a return value. An interface for an event-based version of the ReferralPolicy is shown in Listing 25-19.
要使用清单 25-19 中的IRefferalPolicy,应用服务像往常一样将命令传递给Apply()。但是,要了解命令是否成功,应用服务必须订阅两个事件:ReferralAccepted和ReferralRejected。清单 25-20 说明了此模式。
To use the IRefferalPolicy in Listing 25-19, an application service passes a command into Apply() as usual. However, to learn whether the command was successful, the application service has to subscribe to the two events: ReferralAccepted and ReferralRejected. This pattern is illustrated in Listing 25-20.
对IReferralPolicy事件的订阅发生在RecommendAFriendService的构造函数中,如清单 25-20 所示。每当领域模型触发其中任何一个事件时,都会调用相应的处理程序:HandleReferralAccepted()或HandleReferralRejected()。这些事件是通过将命令传递到领域模型来触发的,就像在RecommendAFriend()内部发生的那样。
Subscription to the IReferralPolicy’s events occurs in the RecommendAFriendService’s constructor shown in Listing 25-20. Anytime the domain model fires either of those events, the appropriate handler is called: HandleReferralAccepted() or HandleReferralRejected(). These events are triggered by passing commands into the domain model, as occurs inside RecommendAFriend().
清单 25-20 中的代码存在一个问题,即它无法正确处理事务,因为事件处理程序无法访问事务对象来提交或回滚。解决这个问题的一个方法是将实例字段作为事务。但是,为了使其正常工作,您必须确保为每个新事务创建一个新的RecommendAFriendService实例,以避免多线程问题。
A problem with the code in Listing 25-20 is that it cannot handle transactions properly because the event handlers do not have access to the transaction object to commit or roll back. A solution to this problem is to have an instance field as a transaction. For this to work, though, you have to ensure that a new instance of the RecommendAFriendService is created for each new transaction to avoid multithreading issues.
清单 25-20 中的代码的另一个与事务相关的问题发生在多线程场景中。考虑使用 C# 任务在另一个线程中异步应用命令的情况:
Another transaction-related problem with the code in Listing 25-20 occurs in multithreading scenarios. Consider the case in which the command is applied asynchronously in another thread using a C# task:
Task.Factory.StartNew(() => policy.Apply(command));
Task.Factory.StartNew(() => policy.Apply(command));
当异步调用 Apply()时,封装此调用的事务作用域不再有效。幸运的是,.NET 的TransactionScope提供了一些模式,可以在异步场景下提供帮助(http://stackoverflow.com/questions/13543254/get-transactionscope-to-work-with-async-await)。在使用此类事务和线程时,仍然需要谨慎。您可能还需要考虑使用async / await。
The transaction scope wrapping this call is no longer in scope when Apply() is called asynchronously. Fortunately, .NET’s TransactionScope has modes that help in async scenarios (http://stackoverflow.com/questions/13543254/get-transactionscope-to-work-with-async-await). When using transactions and threads like this, you still need to be careful. You may also want to consider using async/await.
在选择具有更高复杂性权衡的模式之前,始终值得考虑请求/答复模式的简单性。此模式遵循“一模型进一模型出”方法(OMIOMO),其中将数据传输对象(DTO)传递到应用程序服务并返回一个 DTO,如清单 25-21 所示。
Before choosing patterns with a higher complexity trade-off, it’s always worth considering the simplicity of the request/reply pattern. This pattern follows the One Model In One Model Out Approach (OMIOMO), where a data transfer object (DTO) is passed into the application service and a DTO is returned, as Listing 25-21 demonstrates.
一个常见的约定是,在输入模型的类型后加上“request”作为后缀,在输出模型的类型后加上“response”作为后缀。虽然这是该模式的一个可选方面,但这两个对象都应该是不包含领域对象的简单 DTO,并且都应该驻留在应用服务层中。清单 25-22 中的RecommendAFriendRequest DTO 和清单 25-23 中的RecommendAFriendResponse DTO 体现了这些特征。
A common convention is to suffix the type of the input model with “request” and the output model with “response.” Though this is an optional aspect of the pattern, both objects should be simple DTOs that do not contain domain objects, and both should reside within the application services layer. The RecommendAFriendRequest DTO in Listing 25-22 and the RecommendAFriendResponse DTO in Listing 25-23 exemplify these characteristics.
使用请求/回复时的另一个常见约定是响应对象包含用例的状态。例如,当发生错误或策略被拒绝时,应用服务不会抛出异常,而是会在响应对象上设置相应的状态。
Another common convention when using request/reply is for the response object to contain the status of the use case. When an error occurs or a policy is rejected, for example, instead of the application service throwing an exception, it will set the appropriate status on the response object.
总的来说,请求/回复模式是一种依赖于过程式代码的简单模式。如果您不需要其他复杂模式的优势,最好使用请求/回复模式来保持简单,并让其他开发人员的工作更轻松,直到它开始引起摩擦为止。
Overall, request/reply is a simplistic pattern that relies on procedural code. If you don’t need the benefits of other complex patterns, it’s best to keep things simple and make life easier for other developers by using request/reply until it starts to cause friction.
由于 C# 5 中添加了async和await关键字,C# 开发人员可以编写类似单线程的代码,并享受异步的优势。如果您想构建异步、非阻塞的应用程序,绝对应该考虑它们。然而,由于异步方法需要返回Task<T>类型,这种模式可能会与您对清晰且富有表现力的领域模型的需求相冲突。清单 25-24 演示了这个问题。
C# developers can write single-threaded-like code that has the benefit of being asynchronous thanks to the async and await keywords added in C# 5. You should definitely consider them as an option if you want to build asynchronous, nonblocking applications. This pattern can conflict with your needs for a clear and expressive domain model, though, due to asynchronous methods requiring a return type of Task<T>. Listing 25-24 demonstrates this problem.
在清单 25-24 中,可以看到客户目录返回Task<Customer>。这样一来,实现就可以使用线程效率更高、可扩展性更强的非阻塞数据库调用。显然,这会稍微污染领域。async和await的简洁性确实减轻了语法噪音,如清单 25-25 中的RecommendAFriendService所示。
In Listing 25-24, you can see the customer directory returns Task<Customer>. This is so that an implementation can use nonblocking database calls that are more thread efficient and scalable. Clearly, this pollutes the domain a little. The conciseness of async and await does mitigate the syntactic noise, as the RecommendAFriendService in Listing 25-25 shows.
在RecommendAFriend()中进行这三次调用而不是在领域中进行调用的一个好处是,async / await的技术细节不会扰乱领域逻辑,如清单 25-25 所示。还要注意,在清单 25-25 中,RecommendAFriend()用async关键字标记。这确保了当调用此方法时,如果它包含异步调用,它将异步执行。前两行代码实际上都是异步调用;await关键字表示当 .NET 运行时遇到await关键字时,它会尝试避免阻塞等待调用完成的线程。同样,使用此解决方案时,您在处理事务时需要格外小心 ( http://stackoverflow.com/questions/13543254/get-transactionscope-to-work-with-async-await )。
One benefit of making the three calls in RecommendAFriend() and not the domain is that the technical details of async/await do not clutter the domain logic, as Listing 25-25 illustrates. Also note in Listing 25-25 that RecommendAFriend() is marked with the async keyword. This ensures that when this method is called, it is executed asynchronously if it contains asynchronous calls. Both of the first two lines of code are in fact asynchronous calls; the await keyword signifies this. When the .NET runtime reaches the await keyword, it tries to avoid blocking threads waiting for the invocation to complete. Again, with this solution, you need to take a bit of extra care when working with transactions (http://stackoverflow.com/questions/13543254/get-transactionscope-to-work-with-async-await).
使用async和await时必须权衡利弊。它们的使用可以提高资源效率,但也会给代码增加噪音。幸运的是,在清单 25-25 中,大多数噪音都局限于应用服务而不是领域。这可能是您在自己的设计中可以尽可能追求的折衷方案。
You have to weigh the pros and cons when using async and await. Their use can be more resource efficient, but they also add noise to your code. Fortunately, in Listing 25-25, most of the noise was confined to the application service and not the domain. This might be a compromise that you can aim for in your own designs, where possible.
通过在应用服务级别进行测试,您可以覆盖大量的垂直功能或特性。理想情况下,目标是尽可能多地进行测试,以使环境尽可能接近真实情况。这些类型的测试可以分为多种类别,包括系统测试、验收测试、集成测试和功能测试。
You can cover large vertical slices of functionality or features by testing at the application service level. Ideally, the aim is to test as much as possible so that the environment is as live-like as possible. These types of tests can fall into a variety of groups, including system tests, acceptance tests, integration tests, and functional tests.
通过在 UL 中编写测试,可以成为表达领域概念的绝佳机会。您可以与领域专家一起,使用行为驱动开发 (BDD) 根据他们的验收标准创建测试。这让您有机会与领域专家验证测试的措辞是否与领域概念完全一致。
Tests can be a fantastic opportunity to express domain concepts by writing them in the UL. You may want to sit down with domain experts to create tests based on their acceptance criteria using Behavior Driven Development (BDD). This gives you the opportunity to verify with the domain expert(s) that the wording of your tests aligns precisely with domain concepts.
清单 25-26 显示了应用服务级别的“推荐朋友”用例测试的高级测试大纲。请注意测试名称如何表达领域概念。
A high-level test outline for the Recommend-a-Friend use case testing at the application service level is shown in Listing 25-26. Notice how the names of the tests express domain concepts.
尽可能多地进行测试,可以增强你对应用程序部署时一切正常运行的信心。为此,你需要避免使用模拟和存根,而应该选择改用具体的实现。示例 25-27 展示了如何将其应用于高级的Recommend_a_friend测试。
Testing as much as possible increases your confidence that everything will work together when the application is deployed. To do this, you need to avoid using mocks and stubs, preferring to use concrete implementations instead. Listing 25-27 shows how this applies to the high-level Recommend_a_friend test.
在清单 25-27 中,RecommendAFriendService是用CustomerDirectory的具体实现构造的。这与应用程序部署时使用的实现相同。您可以看到此测试将如何验证CustomerDirectory存储库和RecommendAFriendService应用程序服务是否按要求集成。
In Listing 25-27, the RecommendAFriendService is being constructed with a concrete implementation of the CustomerDirectory. This is the same implementation to be used when the application is deployed. You can see how this test will validate that the CustomerDirectory repository and RecommendAFriendService application service are integrating as required.
您还将注意到,在清单 25-27 中,CustomerDirectory是用内存数据库构建的。这是因为不可能轻松快速地在测试中针对真实数据库进行测试。相反,数据库被替换为易于设置且测试速度快的内存版本。不过,这并不完全像实时测试,因此当数据库相关的东西在运行时可能失败时,测试仍然有很小的可能性通过。在您的套件中添加一些完整的端到端测试,这些测试确实会命中真实的数据库,可以提高您的信心。
You will also notice in Listing 25-27 that the CustomerDirectory is constructed with an in-memory database. This is because it is not possible to test against a real database in a test easily and quickly. Instead, the database is replaced with an in-memory version that is easy to set up and fast to test against. This isn’t completely live-like, though, so there’s still a small chance the test may pass when something database related may fail at run time. Adding a few full end-to-end tests to your suite that do hit a real database can improve your confidence.
另一个无法轻松快速测试的组件是emailer,因此它使用 RhinoMocks 进行存根。这意味着该组件未被覆盖;只要使用正确的参数调用emailer上的方法,此测试就会成功,如清单 25-28 所示。
Another component that cannot be tested easily and quickly is the emailer, so it is stubbed using RhinoMocks. This means that this component is not being covered; this test will succeed as long as methods on the emailer are called with the correct arguments, as shown in Listing 25-28.
RhinoMocks 提供了一个名为AssertWasCalled()的方法,如果在测试运行期间未调用传入的 lambda,则会抛出异常。因此,在清单 25-28 中,RhinoMocks 抛出了一个如果emailer的SendReferralAcknowledgement()没有使用表示引用者的参数调用,则抛出异常。然而,如上所述,当您无法测试完整功能时,模拟和存根通常是最后的手段。当您能够测试完整功能时,您可以直接测试预期结果,如清单25-29所示。
RhinoMocks provides a method called AssertWasCalled() that throws an exception if the passed-in lambda was not invoked during the test run. In Listing 25-28, therefore, RhinoMocks throws an exception if the emailer’s SendReferralAcknowledgement() was not called with an argument that represented the referrer. As mentioned, though, mocks and stubs are often a last resort, when you cannot test the full functionality. When you can test the full functionality, you can directly test the expected outcome, as shown in Listing 25-29.
本章内容是什么?
WHAT’S IN THIS CHAPTER?
本章的 Wrox.com 代码下载
Wrox.com Code Downloads for This Chapter
本章的 wrox.com 代码下载地址为www.wrox.com/go/domaindrivendesign,位于“下载代码”选项卡下。代码位于第 26 章下载中,并根据章节名称单独命名。
The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 26 download and individually named according to the names throughout the chapter.
软件系统旨在满足业务需求。这些需求不仅包括创收功能,还包括评估业务绩效的能力。这就是报表的作用:跟踪重要的指标和关键绩效指标 (KPI),例如销售额、财务目标和客户满意度。正如您在本书中到目前为止所看到的,应用领域驱动设计 (DDD) 构建系统的方法有很多种。同样,实现报表的方法也有很多种。
Software systems are built to support the needs of the business. Not only do these needs include revenue-generating functionality, but they also include the ability to assess how well the business is performing. This is the role of reports: to track important metrics and key performance indicators (KPIs) like sales, financial targets, and customer satisfaction. As you’ve seen so far in this book, there are many ways to build a system when applying Domain-Driven Design (DDD). Equally, there are many ways to implement reporting.
选择如何在应用程序中实现报告功能需要考虑一些常见的权衡因素:开发速度、可维护性、性能,甚至可扩展性。有时,您可以简单地创建一个新的网页来重用所有现有代码。有时,当您拥有分布式有界上下文时,您可能需要创建一个完全独立的报告有界上下文,该上下文订阅来自许多其他有界上下文的事件,并将所有信息存储在本地。本章旨在向您介绍各种选项,让您熟悉这些权衡因素,并更好地帮助您在参与的项目中做出审慎的决策。
Choosing how to implement reporting in your applications involves considering familiar trade-offs: speed of development, maintainability, performance, and even scalability. Sometimes you can simply create a new web page that reuses all your existing code. Other times, when you have distributed bounded contexts, you may need to create an entirely separate reporting bounded context that subscribes to events from many other bounded contexts and stores all the information locally. This chapter aims to present you with a variety of options to make you familiar with the trade-offs and better equip you to make diligent decisions on the projects you are involved in.
将所有数据并置在同一个有界上下文中是最简单的报表场景。这也表明您已正确识别了有界上下文的边界,因为报表通常面向企业内的特定部门,而这些部门应该映射到单个有界上下文。当您无需担心数据分布时,您需要做出的最大决策之一就是是否在生成报表时使用域代码。
Having all your data collocated inside the same bounded context is the least difficult reporting scenario. It’s also an indication that you have correctly identified the boundaries for your bounded contexts, because reports are usually intended for a specific department within a business, which should map onto a single bounded context. One of the biggest decisions you have to make when you don’t have to worry about distribution is whether to use your domain code when generating reports.
要构建显示报表的网页,您可以使用现有的域代码来构建视图模型。如果您需要快速构建页面,这通常是首选方案,因为它所需的工作量最少。最大的缺点是性能,因为您对数据存储区的查询控制较少。如果性能不是主要问题,但开发速度很重要,那么这可能是完美的选择。
To build a web page displaying reports, you can use existing domain code to build a view model. If you need to build a page quickly, this is usually the first option to consider, because it can require the least amount of effort. The big trade-off is performance, because you have less control over the query that is made to your datastore. If performance isn’t much of an issue but development speed is, this can be the perfect choice.
在以下示例中,您将学习如何创建经销商绩效报告,一家汽车特许经营公司将使用该报告来跟踪其经销商的业绩。首先,您将看到一个使用基本映射的示例,然后您将看到一个使用中介者设计模式的替代实现。
In the following examples, you learn how to create a Dealership Performance Report, which an automotive franchise uses to track the performance of its dealerships. First, you see an example using basic mappings, and then you see an alternative implementation using the mediator design pattern.
构建报表的最快方法可能是获取域对象并将其属性映射到视图模型上,该视图模型提供构建视图所需的数据和表示逻辑。鉴于清单 26-1 所示的域对象已在应用程序的其他部分使用,您可以通过简单地映射清单 26-3 所示的属性来轻松填充清单 26-2 所示的视图模型。
Probably the quickest way to build a report is to take domain objects and map their properties onto a view model that provides data and presentation logic required to build the view. Given the domain objects shown in Listing 26-1, which are already used in other parts of the application, you can easily populate the view model shown in Listing 26-2 by simply mapping across the properties shown in Listing 26-3.
从清单 26-1 到 26-3 中的代码可以看出,您可以使用现有的域对象轻松创建报表。您需要做出的最大决策之一是将映射逻辑定位在哪里。您可以将其直接放在应用服务或控制器中,可以创建专用的映射器类,也可以在构造函数中或通过静态工厂方法在视图模型内部进行映射。清单 26-4 展示了此解决方案如何使用名为 DealershipReportBuilder 的应用服务来构建报表。
As you can see from the code in Listings 26-1 to 26-3, you can easily create the report by using existing domain objects. One of the biggest decisions you need to make is where to locate your mapping logic. You can put it directly in application services or controllers, you can create dedicated mapper classes, or you can do the mapping inside the view model in the constructor or via a static factory method. Listing 26-4 shows how this solution looks using an application service called DealershipReportBuilder to build the report.
不幸的是,好处是有代价的。重用清单 26-2 中的域对象使创建报表的工作变得轻松。但是,清单 26-4 中构建视图模型的逻辑可能会因此对性能造成很大的影响。对于每个经销商 ID,都会检索经销商及其性能详细信息。最多可以有三个对象关系映射器(ORM)生成的数据库查询。在一个有 10 家经销商的系统中,可能需要 30 个数据库查询,这在峰值负载下可能会造成严重后果。性能并不总是至关重要的,但在像这样的报告场景中,重要的是要了解你放弃了多少,以及这可能会对你的生产造成什么损害——尤其是当涉及到具有延迟加载等特性的 ORM 时。
Unfortunately, the benefits do come with a cost. Reusing the domain objects from Listing 26-2 made light work of creating the report. However, the logic to build the view model in Listing 26-4 carries a potentially big performance hit because of it. For each dealership ID, the dealership and its performance details are retrieved. There can be up to three object-relational mapper (ORM)-generated database queries. In a system that has ten dealerships, that can be thirty database queries, which may have severe consequences under peak load. Performance isn’t always critical, but in reporting scenarios like this, it’s important to have an idea of how much you are giving away and how that might hurt you in production—especially when ORMs with features like lazy-loading are involved.
使用映射的另一个问题是,您可能需要公开领域对象上的其他属性,而您可能更希望将这些属性保持私有且仅供领域内部使用。因此,这增加了服务层与领域结构耦合的可能性。为了减少服务层和领域之间不必要的耦合,您可能需要考虑其他模式,例如中介模式。
Another problem with using mappings is that you may need to expose additional properties on domain objects that you probably prefer to keep private and internal to the domain. Subsequently, this increases the opportunity for the service layer to be coupled to domain structure. To reduce unwanted coupling between the service layer and the domain, you may want to consider other patterns, like mediator.
要创建一个包含报表所有相关信息但又不与领域结构过度耦合的视图模型,您可以使用中介者设计模式。使用中介者,您可以将视图模型传递给中介者,而中介者本身又会传递到领域结构中。然后,领域对象与中介者交互,中介者会相应地更新视图模型。这不会破坏分层,因为中介者实现了属于领域结构的接口。
To create a view model that contains all the relevant information for a report but isn’t overly coupled to domain structure, you can use the mediator design pattern. Using the mediator, you instead pass your view model into a mediator, which itself is passed into the domain. The domain objects then interact with the mediator, which updates the view model accordingly. This doesn’t break layering, because the mediator implements an interface that belongs in the domain.
作为经销商业绩报告的替代实现的一部分,清单 26-5 展示了中介接口以及修改后的域对象,这些域对象不再公开其结构,而是提供接受和与中介交互的方法。
As part of an alternative implementation of the dealership performance report, Listing 26-5 shows the mediator interface along with modified domain objects that no longer expose their structure, but instead provide a method that accepts and interacts with a mediator.
IDealershipAssessment是清单 26-5 中的中介器接口。每当将中介器的具体实现传递给DealershipPerformanceTargets或DealershipPerformanceActuals的Populate() 方法时,都会使用私有实例变量的值设置中介器上的字段。这样做的好处是这些私有变量不会暴露到领域之外。由于没有了耦合,它们可以自由地更改。这与前面的示例形成了鲜明对比。清单 26-6 中所示的中介器实现试图阐明这一点。
IDealershipAssessment is the mediator interface in Listing 26-5. Whenever a concrete implementation of the mediator is passed into the DealershipPerformanceTargets or DealershipPerformanceActuals’s Populate(), fields are set on the mediator using the values from private instance variables. The benefit to doing this is that those private variables are not exposed outside the domain. Without the coupling, they are free to change. This is in direct contrast to the previous example. The implementation of the mediator shown in Listing 26-6 attempts to clarify this.
在清单 26-6 中,DealershipAssessmentMediator包装了一个DealershipPerformanceStatus视图模型。当中介器被传递给领域对象时,这些领域对象会设置中介器的属性。反过来,中介器会设置它所封装的DealershipPerformanceStatus视图模型的属性。这也是领域和视图模型保持解耦的方式,类似于映射方法。
In Listing 26-6, the DealershipAssessmentMediator wraps a DealershipPerformanceStatus view model. When the mediator is passed into the domain objects, those domain objects set properties on the mediator. In turn, the mediator sets properties on the DealershipPerformanceStatus view model it encapsulates. This is also how the domain and the view model remain decoupled in a similar fashion to the mapping approach.
决定何时使用中介器取决于经验、判断和一些关键标准。如果您希望共享私有域状态,那么中介器应该在您的考虑列表中名列前茅。但是,如果您的域仍在增长,并且不需要中介器带来的额外复杂性,那么它可能不是最佳选择。性能至关重要的报表是另一种场景,由于缺乏底层控制,您可能希望避免使用中介器模式。当性能是一个重要因素时,您可能需要考虑直接使用数据存储区。
Deciding when to use the mediator comes down to experience, judgment, and a few key criteria. If you find yourself wanting to share a private domain state, the mediator should be high on your list of considerations. However, if your domain is still growing and the extra complexity of a mediator is not needed, it’s likely to be a suboptimal choice. Performance-critical reports are another scenario in which you may want to avoid the mediator pattern due to the lack of low-level control. Where performance is a significant factor, you may want consider going directly to the datastore.
当性能和效率至关重要,或者不想经历层层复杂且复杂的映射时,许多 DDD 从业者会直接从数据库提取报表数据。在使用 CQRS 的应用程序中,会为每个需要数据的报表创建专用的非规范化数据副本。当应用程序不使用 CQRS 时,通常使用 ADO.NET 等原始数据访问技术来查询数据存储区。但使用 ORM 的低级功能(例如 NHibernate 的 HQL)也很常见。
When performance and efficiency are important, or when going through layers of complexity and mappings is not desired, many DDD practitioners pull data for their reports directly from the database. In applications that use CQRS, dedicated, denormalized copies of the data are created for each report that needs them. When applications don’t CQRS, it’s common to query the datastore using raw data access technologies like ADO.NET. But it’s also common to use low-level features of ORMs, such as NHibernate’s HQL.
在本节中,您将看到一个使用临时报告查询来查询项目主数据存储的示例。之后,您将看到一个查询数据的非规范化副本(视图缓存)的示例,该副本专门用于报告。每个示例都涉及为在线体育用品商店创建一份忠诚度报告。该报告向企业表明其忠诚度计划的成功程度。表 26.1显示了忠诚度报告的格式。
In this section, you see an example of querying a project’s main datastore with an ad-hoc reporting query. After that, you see an example of querying a denormalized copy of the data (a view cache), used specifically for reporting. Each of these examples involves creating a loyalty report for an online sports store. This report indicates to the business how successful its loyalty program is. Table 26.1 shows the format of the loyalty report.
表 26.1忠诚度报告的显示格式
TABLE 26.1 Display Format of the Loyalty Report
| 积分(每$) | 净利润(占整体的百分比) | 注册 | 购买量(占总量的百分比) | |
| A月 | … | … | … | … |
| B月 | … | … | … | … |
了解忠诚度计划产生的利润是忠诚度报告最重要的要求。如表 26.1所示,这通过显示特定月份忠诚度计划产生的总利润百分比来实现。作为忠诚度优化策略的一部分,也为了与竞争对手竞争,这家在线体育用品商店经常会调整奖励积分的数量。利用忠诚度报告,企业可以推断出改变这一比例将如何影响计划的整体成功。最后,如果没有虚荣指标,任何报告都是不完整的,因此忠诚度报告还会显示忠诚度计划的注册人数。
Understanding how much profit the loyalty scheme is generating is the most important requirement of the loyalty report. As Table 26.1 shows, this is achieved by showing what percentage of overall profit came from the loyalty scheme for a given month. As part of their loyalty-optimization strategy, and to compete with rival companies, the online sports store often adjusts the number of points awarded. Using the loyalty report, the business can draw inferences about how changing this ratio affects the overall success of the scheme. Finally, no report would be complete without vanity metrics, so the loyalty report shows the number of loyalty scheme sign-ups as well.
通过直接查询数据存储区来构建报告可以让您更好地控制并编写高效的查询。要生成表 26.1中所示的忠诚度报告,SQL 查询可能需要连接并提取多个表的数据,包括orders、users、lotityAccounts、lotitySettings甚至更多。许多团队发现,相信 ORM 执行像这样的具有大量连接的复杂查询无异于自取灭亡。因此,Micro-ORM 变得非常流行,因为它们提供了 Big-ORM 带来的一些好处,同时又减少了很多复杂性。Micro-ORM 比 Big-ORM 的抽象级别更低,为您提供了对查询的更多控制,并为您提供了更好的机会使查询快速高效。
Building reports by directly querying a datastore gives you greater control and the ability to write efficient queries. To generate the loyalty report shown in Table 26.1, a SQL query may need to join and pull in data from a number of tables, including orders, users, loyaltyAccounts, loyaltySettings, and maybe even more. Many teams find that trusting an ORM to perform complex queries with lots of joins like this is a recipe for disaster. As a result, Micro-ORMs have become very popular because they provide some of the benefits Big-ORMs bring, yet they cut out a lot of the complexity. Micro-ORMs are a lower level of abstraction than Big-ORMs, providing you with more control over your queries and a better opportunity to make them fast and efficient.
清单 26-7 展示了一个应用服务,它使用 Dapper(https://code.google.com/p/dapper-dot-net/)(一个简洁的 Micro-ORM)直接针对项目的主 SQL 数据库运行 SQL 查询,而不涉及域。您还可以在清单 26-8 中看到映射的视图和数据库模型的定义。
LISTING 26-7 shows an application service that uses Dapper (https://code.google.com/p/dapper-dot-net/), a concise Micro-ORM, to run a SQL query directly against the project’s main SQL database without involving the domain. You can also see the definition of the view and database models being mapped to and from in Listing 26-8.
Dapper 将Query<T>扩展方法添加到本机 ADO.NET SQLConnection上,如清单 26-7 所示。Query <T>将传递的查询结果映射到它为您创建的T类型的对象上。但清单 26-8 中的重要问题是开发人员完全控制生成的 SQL。在性能关键型报告场景中,低级数据访问规避了 ORM 相关的低效率问题。然而,更强大的控制力是以潜在的概念重复为代价的。
Dapper adds the Query<T> extension method onto the native ADO.NET SQLConnection, as shown in Listing 26-7. Query<T> maps the results of the query you pass onto an object of type T that it creates for you. But the important issue in Listing 26-8 is that the developer is completely in control of the SQL being generated. In performance-critical reporting scenarios, low-level data access circumvents the inefficiency associated with ORMs. Unfortunately, greater control comes at the cost of potential concept duplication.
直接数据存储查询的挑战之一是重复。一些领域实体具有计算属性。如果查看清单 26-7 中profitQuery的 SQL ,您可以看到忠诚度净利润相对于同一时期的整体净利润的百分比。这很可能也是领域模型中某个地方发生的计算。这是一种风险,并且违反了“不重复自己”(DRY)原则,因为如果这个计算由于任何原因发生变化,报告中的 SQL 查询和领域逻辑都需要更新——这很容易被忽视或遗忘。
One of the challenges with direct-datastore queries is duplication. Some domain entities have computed properties. If you look at the SQL for the profitQuery in Listing 26-7, you can see the percentage of loyalty net profit being calculated against overall net profit in the same period. This is likely to be a calculation that occurs somewhere in the domain model as well. It’s a risk and a violation of the don’t repeat yourself (DRY) principle, because if this calculation were to change for any reason, both the SQL query in the report and the domain logic would both need to be updated—something that can easily be overlooked or forgotten.
在任何地方复制域逻辑都不是理想的做法。显然,如果您更新了域逻辑,却忘记更新数据存储区查询中的逻辑,则可能会遇到一些问题,这些问题会令用户感到烦恼,或者给业务部门提供完全错误的数字。如果您担心这个问题,您可能还需要考虑存储计算属性的值。在这种情况下,您需要计算计算属性的值,然后在每次更新时将其保存到数据库中。但是,如果您在多个位置更新数据库,则需要在多个位置重新计算该值,或者使用数据库触发器。
Duplicating domain logic anywhere is not ideal. Clearly, if you update the logic in the domain and forget to update the logic in the datastore query, you can have several problems that annoy users or give the business completely wrong numbers. If you’re worried about that concern, you may also want to consider storing the value of the computed property. In this scenario, you need to calculate the value of the computed property and then save it to the database whenever there is an update. However, if you update the database in multiple places, you need to recompute the value in multiple places or have a database trigger.
有时,即使直接使用手工编写的 SQL 查询数据库也可能效率低下。因此,一些 DDD 从业者选择创建特定于视图/报告的非规范化数据副本(视图缓存)。这在概念和实现上与 CQRS 类似。每当发生更新时,主数据库都会更新,但相关的非规范化视图缓存也会更新。图 26.1展示了如何为忠诚度报告实现此模式。
Sometimes, even directly querying the database with handcrafted SQL can be inefficient. For this reason, some DDD practitioners choose to create view/report-specific denormalized copies of the data (view caches). This is close in concept and implementation to CQRS. Whenever an update occurs, the main database is updated, but so are the relevant denormalized view caches. Figure 26.1 shows how you can implement this pattern for the loyalty report.
图 26.1忠诚度报告的非规范化视图缓存。
FIGURE 26.1 Denormalized view cache for the loyalty report.
随着订单的下达和新用户的注册(表现为方法调用、命令、领域事件等),领域会照常被调用。然而,在创建非规范化视图时,更新通常遵循一条路径到达主数据库,并至少通过另一条路径通过非规范化器到达非规范化视图缓存,如图26.1所示。非规范化器的工作通常是将数据扁平化,以便查询成为简单的 SQL 选择语句。清单 26-9 展示了另一种LoyaltyReportBuilder,它从非规范化视图中提取数据,通过将复杂性转移到非规范化器,强调了查询可以多么简单。
As orders are placed and new users sign up (manifested as method calls, commands, domain events, and so on), the domain is invoked as usual. When creating denormalized views, though, the updates usually follow one path to the main database and at least one other via a denormalizer to the denormalized view cache, as per Figure 26.1. The denormalizer’s job is usually to flatten the data so that queries are simple SQL select statements. Listing 26-9 shows an alternative LoyaltyReportBuilder that pulls in data from a denormalized view, emphasizing just how simple the query can be, by offloading the complexity to a denormalizer.
如清单 26-9 所示,复杂度大幅降低,只需一个 SQL 选择,无需连接。这全都归功于前期对数据进行非规范化的额外努力。在使用此方法之前,您必须确定这种努力是否足以降低项目的复杂性或提升项目的性能。不过,您可以在项目中适当地混合搭配使用。
As you can see in Listing 26-9, the complexity is massively reduced to just a single SQL select without joins. This is all thanks to extra up-front effort of denormalizing the data. You have to decide if that effort provides enough of a reduction in complexity or enough of a performance improvement on your projects before using this approach. You can mix and match where appropriate on your projects, though.
使用事件源(在第 22 章“事件源”中介绍)的应用程序需要使用不同的技术来生成报告,因为它们不存储应用程序状态的当前表示。相反,事件源应用程序依赖于一种称为投影的功能。投影实际上只是针对事件流的查询,它根据原始流中事件的内容生成一些所需的状态或新的流。
Applications that use event sourcing, which was introduced in Chapter 22, “Event Sourcing,” require a different technique to generate reports because they don’t store the current representation of the application state. Instead, event-sourced applications rely on a feature called projections. Projections are really just queries against event streams that produce some desired state or new streams, based on the contents of the events in the original stream.
以下示例将演示如何在报告中使用投影,其中投影用于创建医疗保健诊断报告。一家医疗保健机构使用此报告来跟踪每月针对某些医疗状况的诊断数量。您可以在表 26.2中看到此报告的格式。
Projection usage in a reporting context will be demonstrated in the following examples, where projections are used to create a health care diagnosis report. A health care authority uses this report to track the number of diagnoses made for certain medical conditions on a monthly basis. You can see the format of this report in Table 26.2.
表 26.2医疗保健诊断报告格式
TABLE 26.2 Health Care Diagnosis Report Format
| 2014年2月 | 2014年3月 | 2014年4月 | 2014年5月 | |||||
| 全部的 | % | 全部的 | % | 全部的 | % | 全部的 | % | |
| 诊断A | — | — | — | — | — | — | — | — |
| 诊断B | — | — | — | — | — | — | — | — |
如表 26.2所示,医疗保健诊断报告中的每一行都跟踪了每月诊断的次数。每个月,都会显示诊断数量及其占当月所有诊断的百分比。医疗保健基金会的工作人员可以通过此报告查找某些诊断的趋势。这可能有助于他们了解季节性差异,或将变化与其他事件(例如新疫苗或医疗实践的推出)关联起来。
As Table 26.2 shows, each row in the health care diagnosis report tracks the number of times a diagnosis is made each month. For each month, the number of diagnoses made is shown alongside its percentage relative to all diagnoses made in that month. Using this report, the staff at the Health Care Foundation can look for trends in certain diagnoses. This may help them understand seasonal differences or correlate changes with other events such as the introduction of new vaccines or medical practices.
为了实现此报告,每个诊断的每个月度摘要都会创建一个新的事件流,其命名格式为diagnosis-{diagnosisId}-{month}。这些新流是通过对包含所有诊断的单个事件流(“诊断”流)进行操作的投影创建的。图 26.2说明了这个过程。
To implement this report, each monthly summary, for each diagnosis, is created as a new event stream with the naming format diagnosis-{diagnosisId}-{month}. These new streams are created from a projection that operates on a single event stream containing every diagnosis (the “diagnoses” stream). Figure 26.2 illustrates this process.
图 26.2将“诊断”事件流投影到代表每个诊断的每月摘要的事件流上。
FIGURE 26.2 Projecting the “diagnoses” event stream onto event streams representing the monthly summary of each diagnosis.
对于每个诊断,流都包含其每个月的所有事件,如图26.2所示。例如,2014 年 2 月针对 ID 为dg1的诊断所做的所有诊断都被投影到流diagnosis-dg1_201402中。这意味着,当您构建报告时,您只需计算流中的事件数即可获得该月的总数。如您所见,使用投影涉及与创建非规范化视图缓存类似的理念——所有艰苦的工作都是预先完成的,以减少读取数据的复杂性。
For every diagnosis made, a stream contains all its events for each month, as Figure 26.2 shows. As an example, all diagnoses made for the diagnosis with ID dg1 in February 2014 are projected into the stream diagnosis-dg1_201402. This means that when you come to build the report, all you have to do is count the number of events in a stream to get the total for that month. As you can see, using projections involves a similar philosophy to creating denormalized view caches—all the hard work is done up front to reduce the complexity involved in reading the data.
要完成本节中的示例,您需要 Event Store v3 rc2 才能利用较新的投影功能。因此,您需要下载 Event Store(http://download.geteventstore.com/binaries/EventStore-OSS-Win-v3.0.0-rc2.zip),将其解压到您选择的文件夹中,然后在解压后的目录中,以管理员身份通过 PowerShell 运行以下启动命令:
To work through the examples in this section, you need Event Store v3 rc2 to take advantage of newer projection capabilities. So you need to download the Event Store (http://download.geteventstore.com/binaries/EventStore-OSS-Win-v3.0.0-rc2.zip), extract it into a folder of your choice, and then run the following start-up command from PowerShell (as Administrator) from inside the directory you extracted the Event Store to:
.\EventStore.SingleNode.exe ––db .\ESData ––run-projections=all
.\EventStore.SingleNode.exe ––db .\ESData ––run-projections=all
事件存储启动后,您需要对其配置进行一些更改,以启用一些投影功能。您可以通过导航到“Projections”选项卡来进行这些更改。浏览器(http://localhost:2113/projections)并启动投影$by_category和$stream_by_category。
Once the Event Store is started, you need to make a few changes to its configuration that enable some projection features. You can make these changes by navigating to the Projections tab in your browser (http://localhost:2113/projections) and starting the projections $by_category and $stream_by_category.
投影是使用 JavaScript 创建的,可以发布到超文本传输协议(HTTP)应用程序编程接口(API),也可以手动输入到管理网站。本示例使用后一种方法,您可以先导航到【Projections】选项卡,然后选择【New Projection】。因为您需要创建的投影按月份对诊断的所有事件进行分组,所以它被称为【DiagnosesByMonth】。此投影的代码如清单26-10所示,需要添加到源输入编辑器中。创建【DiagnosesByMonth】时,需要选择【Continuous】模式并选中【Emit Enabled】复选框。完成后,您可以单击【Post】来创建投影。
Projections are created using JavaScript, which can either be posted to the Hypertext Transport Protocol (HTTP) application programming interface (API) or manually entered into the admin website. This example uses the latter approach, which you can carry out by first navigating the Projections tab and then choosing New Projection. Because the projection you need to create groups all events for a diagnosis by month, it is called DiagnosesByMonth. The code for this projection is shown in Listing 26-10 and needs to be added to the Source input editor. When creating the DiagnosesByMonth, you need to select Continuous mode and check the Emit Enabled check box. Once this is complete, you can click Post to create the projection.
事件存储将投影应用于流中的每个事件。因此,清单 26-10 中的 JavaScript 应用于诊断流中的每个事件。每个事件都将在另一个流中创建对该事件的引用。该另一个流表示同一个月内具有相同诊断 ID的所有诊断。这是 26-2 中演示的过程。事件存储通过其linkTo()支持此投影行为。linkTo在名称与第一个参数匹配的流上添加对作为第二个参数传入的事件的引用,并在必要时创建该流。因此,投影实际上并不复制事件;它们只是创建引用或指针。
The Event Store applies projections to each event in the stream. So the JavaScript in Listing 26-10 is applied to each event in the diagnoses stream. Each of those events is going to create a reference to the event in another stream. That other stream represents all diagnoses with the same diagnosisId in the same month. This is the process that was illustrated in 26-2. The Event Store supports this projection behavior with its linkTo(). linkTo adds a reference to the event passed in as the second argument on the stream whose name matches the first argument, creating that stream if necessary. Therefore, projections do not actually copy events; they just create references or pointers.
要检查投影是否有效,您可以导航到事件存储的“流”选项卡,并观察新创建事件的名称。您应该会看到格式为diagnosis-{diagnosisId}_{month}的事件,例如diagnosis-d13_201402。如果您点击其中一个流,您会看到指向位于诊断流中的事件的指针,而投影正是基于该诊断流。
To check that the projection has worked, you can navigate to the Event Store’s Streams tab and observe the names of newly created events. You should see events of the format diagnosis-{diagnosisId}_{month}, such as diagnosis-d13_201402. If you click on one of these streams, you see pointers to events that reside in the diagnoses stream, which the projection is based on.
报告中的每一行都需要显示每个月的诊断数量。如前所述,这些总数只是清单 26-10 中投影创建的每个流中的事件数。查询事件流大小的一种方法是创建另一个投影。这是许多人推荐的方法,在本例中将使用它。
Each row in the report needs to show the number of diagnoses made in each month. As discussed previously, these totals are just the number of events in each stream created by the projection in Listing 26-10. One approach for querying the size of an event stream is to create another projection. It’s an approach that many recommend, and it will be used in this example.
要创建计算每个诊断每月总数的投影,需要使用清单 26-11 中的 JavaScript。为了遵循此示例,将此投影命名为DiagnosesByMonthCounts。它应该再次使用连续模式,但您可以取消选中 Emit Enabled 。然后,单击 Post 即可创建投影。
To create the projection that counts the monthly total for each diagnosis, you need to use the JavaScript in Listing 26-11. To follow along with this example, name this projection DiagnosesByMonthCounts. It should again use the continuous mode, but you can leave Emit Enabled unchecked. The projection is then ready to be created by clicking Post.
在事件存储中,类别是具有相同前缀的流。前缀是连字符前面的文本字符串。因此,所有由第一个投影创建的以diagnosis-开头的流都属于诊断类别。类别提供了清单 26-11 中投影行为的功能。foreachStream ()对类别中的每个流进行操作,因此清单 26-11 中的投影会遍历诊断类别中的每个流并计算有多少个事件。此计数存储在投影的状态中。您可以通过查询投影的状态来确认这一点,确保提供您想要获取状态的流的名称作为分区参数的值。例如,对http://localhost:2113/projection/DiagnosesByMonthCounts/state?partition=diagnosis-dg1_201402 的请求将以以下格式获取该流中所有事件的计数:
In the Event Store, categories are streams that have the same prefix. A prefix is a string of text preceding a hyphen. So all the streams that begin diagnosis- created by the first projection are in the diagnosis category. Categories provide the capability for the behavior of the projection in Listing 26-11. foreachStream() operates on each stream in a category, so the projection in Listing 26-11 goes through each stream in the diagnosis category and counts how many events there are. This count is stored in the projection’s state. You can confirm this by querying the state for the projection, making sure to supply the name of the stream you want the state for as the value of the partition parameter. For example, a request for http://localhost:2113/projection/DiagnosesByMonthCounts/state?partition=diagnosis-dg1_201402 gets the count of all events in that stream in the following format:
{
count:1
}
{
count:1
}
使用事件存储,创建流通常是一项廉价的操作,正如第 22 章所述。因此,获取任何给定月份的诊断总数是一个使用投影来创建更多流的机会。创建这些流遵循与前两个相同的模式。首先,可以使用清单 26-12 所示的 JavaScript 按月份对事件进行分区,使用与DiagnosesByMonth投影的设置相同。为了继续本例,我们将此投影命名为Months。
With the Event Store, creating streams is usually a cheap operation, as was mentioned in Chapter 22. So getting the total number of diagnoses made in any given month is an opportunity to use projections that create further streams. Creating these streams follows the same pattern as the last two. First, events can be partitioned by month using the JavaScript shown in Listing 26-12, using the same settings as the DiagnosesByMonth projection. To follow along with this example, call this projection Months.
一旦运行了Months投影,您只需要以与DiagnosesByMonthCounts投影相同的方式对每个流中的数字进行求和。您可以在清单 26-13 中看到此投影的代码。它看起来很熟悉。一旦此投影运行,构建报告所需的所有流都将到位。
Once you have run the Months projection, you then just need to sum up the numbers in each stream in the same way as the DiagnosesByMonthCounts projection. You can see the code for this projection in Listing 26-13. It should look familiar. Once this projection is running, all the streams that are needed to build the report will be in place.
通过一组包含所有所需数据的事件流,构建报告的过程简化为一系列 HTTP 调用(或与客户端库的交互)和对象之间的映射。在当前示例的最后一部分,名为HealthcareReportBuilder 的应用服务演示了这一点。清单 26-14 展示了HealthcareReportBuilder的初始版本,其中包含构建报告所需的高级逻辑。
With a set of event streams containing all the needed data, building the report is reduced to a series of HTTP calls (or interactions with a client library) and mapping between objects. An application service called HealthcareReportBuilder demonstrates that in this final part of the current example. Listing 26-14 shows the initial version of the HealthcareReportBuilder containing the high-level logic required to build the report.
要构建HealthcareReport,HealthcareReportBuilder首先计算指定日期范围内的每个月。对于每个月,它首先从事件存储中获取诊断总数,并调用FetchMonthlyTotalsFromES() ,其实现如清单 26-15 所示。
To build the HealthcareReport, the HealthcareReportBuilder starts by calculating each month in the specified date range. For each of those months, it first fetches the total number of diagnoses from the Event Store, with the call to FetchMonthlyTotalsFromES() whose implementation is shown in Listing 26-15.
为了获取每个月的总数,清单 26-15 中的代码为MonthsCounts投影的状态资源构建了一个 URL。包含该月所有诊断的流的名称用作分区值。作为响应,事件存储 API 将计数以 JSON 格式返回。您可以看到此 JSON 响应被映射到DiagnosisCount,它是一个与 JSON 响应结构匹配的数据传输对象(DTO),如清单 26-16 所示。然后,此对象的Count属性将存储为该月的计数。当给定月份没有诊断时,也没有计数值。在这些情况下,代码将值设置为零。
To get the total for each month, the code in Listing 26-15 constructs a URL for the MonthsCounts projection’s state resource. The name of the stream containing all the diagnoses for that month is used as the partition value. In response, the Event Store API returns the count as JSON. You can see this JSON response being mapped onto a DiagnosisCount, which is a data transfer object (DTO) that matches the structure of the JSON response, as Listing 26-16 shows. This object’s Count property is then stored as the count for the month. When there are no diagnoses for a given month, there are no count values either. The code sets a value of zero in those cases.
在获得每个月的总计后,HealthcareReportBuilder将获取每个诊断的每月总计。不过,在查询事件存储之前,它通过调用BuildQueriesFor()执行了一个中间步骤。BuildQueriesFor ()创建一个强类型 DTO 集合,其格式如清单26-17所示,以使代码更具表现力。
After obtaining the overall totals for each month, the HealthcareReportBuilder then gets the monthly total for each diagnosis. Before querying the Event Store, though, it carries out an intermediate step with the call to BuildQueriesFor(). BuildQueriesFor() creates a collection of strongly typed DTOs of the format shown in Listing 26-17 to make the code more expressive.
创建DiagnosisQueries集合后,HealthcareReportBuilder使用它们最终查询事件存储中每个诊断的每月总数,并调用BuildMonthlySummariesFor()。它的实现类似于FetchMonthlyTotalsFromES(),因为实际的困难工作是查询事件存储并映射响应,如清单26-18所示。
After creating the collection of DiagnosisQueries, the HealthcareReportBuilder uses them to finally query the Event Store for the monthly totals for each diagnosis, with the call to BuildMonthlySummariesFor(). The implementation of this is similar to FetchMonthlyTotalsFromES() in that the actual hard work is querying the Event Store and mapping the response, as shown in Listing 26-18.
除了获取总数之外,BuildMonthlySummaries()还使用先前获取的月总数计算百分比,并将结果映射到DiagnosisSummary上。完成后,每个DiagnosisSummary都映射到HealthcareReport视图模型上,如清单26-14所示。所有这些艰苦的工作都完成了,您可以呈现报告了。
Aside from fetching the totals, BuildMonthlySummaries() calculates the percentages, using the monthly total previously fetched, and maps the results onto a DiagnosisSummary. Upon completion, each DiagnosisSummary is mapped onto the HealthcareReport view model, as shown in Listing 26-14. All this hard work is then complete, and you can render the report.
遗憾的是,生成报告并不总是像查询单个数据存储那么简单。当您拥有一个分布式系统时,例如第二部分“战略模式:分布式领域驱动设计模式”中讨论的系统,每个有界上下文都有自己的数据存储,需要您额外完成一些工作才能生成报告。本节概述了两种依赖于前几章中介绍的技术的方法。一种方法是使用第十二章的事件驱动原则来创建一个专用的报告有界上下文,该上下文订阅大量事件,并在单个数据库中本地存储所需的所有信息。不过,有时您也可以采用一种更轻松的方法,例如使用第二十三章“组合应用程序”中概述的 UI 组合技术。
Unfortunately, producing reports is not always as easy as querying a single datastore. When you have a distributed system, such as those discussed in Part II, “Strategic Patterns: Patterns for Distributed Domain-Driven Design,” each bounded context has its own datastore(s) that requires additional work on your behalf to produce the reports. This section outlines two approaches that rely on techniques presented in earlier chapters. One approach is to use the event-driven principles of Chapter 12 to create a dedicated reporting bounded context that subscribes to lots of events having all the information it needs locally in a single database. Sometimes, though, you can get away with a much lighter approach, using the UI composition techniques outlined in Chapter 23, “Composing Applications.”
将来自多个有界上下文的数据合并形成报告是可行的,但通常只有当大部分处理过程可以分阶段进行,且每个阶段都由单个有界上下文执行时才有效。任何其他支持信息,例如将 ID 转换为名称,也可以在之后通过查询拥有查询源的有界上下文来执行。可以使用一份地区唱片公司比较报告来证明这一点。一家在线音乐流媒体组织可以使用此报告显示各个唱片公司在不同国家/地区的受欢迎程度。受欢迎程度衡量的是唱片公司旗下每首歌曲的流媒体播放量和下载量的总和。表 26.3显示了地区唱片公司比较报告的布局。
Combining data from multiple bounded contexts to form a report can work, but usually only when most of the processing can be carried out in distinct phases, each by a single bounded context. Any other supporting information, like translating IDs to names, can also be carried out afterward by querying the bounded context that owns the source of the lookup. A territorial record label comparison report can be used to demonstrate this. An online music streaming organization can use this report to show the popularity of each record label in a variety of countries. Popularity is a measure of the combined total of streams and downloads for every song belonging to a record label. Table 26.3 shows the layout of a territorial record label comparison report.
表 26.3地区记录标签比较报告
TABLE 26.3 Territorial Record Label Comparison Report
| 北美 | 欧洲 | 亚洲 | |
| 唱片公司 1 | — | — | — |
| 唱片公司 2 | — | — | — |
| 唱片公司 3 | — | — | — |
制作区域唱片公司比较报告的一大挑战是,流媒体和下载是业务中完全独立的部分,各自都有各自的限界上下文。因此,要获得每个唱片公司的流媒体和下载总量,需要将来自每个限界上下文的信息合并起来,如图26.3所示。
One of the big challenges involved in producing the territorial record label comparison report is that streaming and downloads are completely independent parts of the business, each with its own bounded context. So to get the total of streams and downloads for each record label, the information from each of those bounded contexts needs to be combined, as Figure 26.3 shows.
图 26.3将来自多个有界上下文的数据聚合到一份报告中。
FIGURE 26.3 Aggregating data from multiple bounded contexts into a single report.
幸运的是,聚合可以分阶段进行。总下载量可以从“下载”限界上下文中检索。同时,每个唱片公司的流媒体总数也可以从“流媒体”限界上下文中检索。使用客户端或服务器端聚合(如第 23 章所示),可以轻松地合并每个地区每个唱片公司的总数。
Fortunately, the aggregation can occur in distinct phases. Total downloads can be retrieved from the Downloads bounded context. At the same time, the total number of streams for each label can be retrieved from the Streaming bounded context. Using client- or server-side aggregation, as demonstrated in Chapter 23, the totals for each record label in each territory can easily be combined.
出于性能、效率或便利性的考虑,将所有用于报告的数据实时存储在同一个数据存储区中可能是一个重要的标准。数据仓库就是一个例子,企业希望以新的方式对所有数据进行切分,以发现洞察。企业通常会聘请数据科学家来承担这一重要角色。阅读第二部分后,在“战略模式:分布式领域驱动设计模式”一文中,你了解构建分布式有界上下文的默认方式,因为每个有界上下文都有自己的数据存储,并且与其他上下文松耦合。但你也知道,有界上下文通过事件进行通信,这为创建一个特殊的报告上下文提供了可能性,该上下文可以订阅来自多个有界上下文的事件,从而收集所需的所有数据。
For reasons of performance, efficiency, or convenience, having all your data for reporting live inside the same datastore may be an important criteria. One example of this is data warehousing, in which the business wants to slice and dice all its data in new ways as it seeks to uncover insights. Often a business employs data scientists to carry out this important role. Having read Part II, “Strategic Patterns: Patterns for Distributed Domain-Driven Design,” about building distributed bounded contexts, you know that by default this is not possible due to each bounded context having its own datastore(s) and being loosely coupled to the others. But you also know that bounded contexts communicate with events, opening the possibility to create a special reporting context that subscribes to events from many bounded contexts, enabling it to gather all the data it needs.
报表上下文的实现在范围和实现方式上可能存在巨大差异。在最简单的情况下,它可能像任何其他有界上下文一样,订阅事件并将其存储在 SQL 数据库中,如图 26.4所示。另一方面,它可能通过各种数据库技术、推荐引擎和机器学习算法推送数据,类似于 Netflix(http://techblog.netflix.com/2013/01/hadoop-platform-as-service-in-cloud.html),如图26.5所示。
Implementing a report context can vary drastically in scope and implementation. In the simplest case, it may be like any other bounded context in that it subscribes to events and stores them in a SQL database, as shown in Figure 26.4. At the other end of the scale, it may be pushing data through a variety of database technologies, recommendation engines, and machine learning algorithms, similar to Netflix (http://techblog.netflix.com/2013/01/hadoop-platform-as-service-in-cloud.html), as Figure 26.5 illustrates.
图 26.4标准报告背景。
FIGURE 26.4 Standard reporting context.
图 26.5复杂的数据处理报告环境。
FIGURE 26.5 Complex data-processing reporting context.
要了解有关事件驱动的面向服务架构 (SOA) 系统中的报告和商业智能的更多信息,Arnon Rotem-Gal-Oz 在 InfoQ 网站上发表了一篇详细的文章 ( http://www.infoq.com/articles/BI-and-SOA )。
To learn more about reporting and business intelligence in an event-driven Service Oriented Architecture (SOA) system, Arnon Rotem-Gal-Oz has published a detailed article on the InfoQ website (http://www.infoq.com/articles/BI-and-SOA).
领域驱动设计的模式、原则和实践
Patterns, Principles, and Practices of Domain-Driven Design
由John Wiley & Sons, Inc.出版,
地址:10475 Crosspoint Boulevard
Indianapolis, IN 46256,
网址:www.wiley.com
Published by
John Wiley & Sons, Inc.
10475 Crosspoint Boulevard
Indianapolis, IN 46256
www.wiley.com
版权所有 © 2015 John Wiley & Sons, Inc.,印第安纳波利斯,印第安纳州
Copyright © 2015 by John Wiley & Sons, Inc., Indianapolis, Indiana
在加拿大同时出版
Published simultaneously in Canada
ISBN: 978-1-118-71470-6
ISBN: 978-1-118-71465-2 (ebk)
ISBN: 978-1-118-71469-0 (ebk)
ISBN: 978-1-118-71470-6
ISBN: 978-1-118-71465-2 (ebk)
ISBN: 978-1-118-71469-0 (ebk)
美国制造
Manufactured in the United States of America
10 9 8 7 6 5 4 3 2 1
10 9 8 7 6 5 4 3 2 1
未经出版商事先书面许可,或通过向版权许可中心(地址:222 Rosewood Drive, Danvers, MA 01923,电话:(978) 750-8400,传真:(978) 646-8600)支付相应的每份费用获得授权,不得以任何形式或任何手段(电子、机械、影印、录制、扫描或其他方式)复制、存储于检索系统或传播本出版物的任何部分,除非《1976 年美国版权法》第 107 或 108 条允许。向出版商申请许可,请寄往约翰威利父子公司许可部门,地址:111 River Street, Hoboken, NJ 07030,电话:(201) 748-6011,传真:(201) 748-6008,或在线访问http://www.wiley.com/go/permissions。
No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, scanning or otherwise, except as permitted under Sections 107 or 108 of the 1976 United States Copyright Act, without either the prior written permission of the Publisher, or authorization through payment of the appropriate per-copy fee to the Copyright Clearance Center, 222 Rosewood Drive, Danvers, MA 01923, (978) 750-8400, fax (978) 646-8600. Requests to the Publisher for permission should be addressed to the Permissions Department, John Wiley & Sons, Inc., 111 River Street, Hoboken, NJ 07030, (201) 748-6011, fax (201) 748-6008, or online at http://www.wiley.com/go/permissions.
责任限制/免责声明:出版商和作者对本作品内容的准确性或完整性不作任何陈述或保证,并明确否认所有保证,包括但不限于适用于特定用途的保证。销售或宣传材料不构成或延长任何保证。本文包含的建议和策略可能并非适用于所有情况。出售本作品时,出版商不提供法律、会计或其他专业服务。如果需要专业协助,应寻求有能力的专业人士的服务。出版商和作者均不对由此造成的损害承担责任。本作品中引用某个组织或网站作为引文和/或潜在信息来源,并不意味着作者或出版商认可该组织或网站可能提供的信息或可能提出的建议。此外,读者应注意,本作品中列出的互联网网站可能在撰写本作品和阅读本作品期间已更改或消失。
Limit of Liability/Disclaimer of Warranty: The publisher and the author make no representations or warranties with respect to the accuracy or completeness of the contents of this work and specifically disclaim all warranties, including without limitation warranties of fitness for a particular purpose. No warranty may be created or extended by sales or promotional materials. The advice and strategies contained herein may not be suitable for every situation. This work is sold with the understanding that the publisher is not engaged in rendering legal, accounting, or other professional services. If professional assistance is required, the services of a competent professional person should be sought. Neither the publisher nor the author shall be liable for damages arising herefrom. The fact that an organization or Web site is referred to in this work as a citation and/or a potential source of further information does not mean that the author or the publisher endorses the information the organization or Web site may provide or recommendations it may make. Further, readers should be aware that Internet Web sites listed in this work may have changed or disappeared between when this work was written and when it is read.
有关我们其他产品和服务的一般信息,请联系我们的客户服务部门(美国境内请致电 (877) 762-2974,美国境外请致电 (317) 572-3993 或传真 (317) 572-4002)。
For general information on our other products and services please contact our Customer Care Department within the United States at (877) 762-2974, outside the United States at (317) 572-3993 or fax (317) 572-4002.
Wiley 以多种印刷版、电子版和按需印刷形式出版。本书标准印刷版中包含的部分内容可能不包含在电子书或按需印刷版中。如果您购买的版本中未包含本书所引用的 CD 或 DVD 等介质,您可以从http://booksupport.wiley.com下载这些内容。如需了解更多关于 Wiley 产品的信息,请访问www.wiley.com。
Wiley publishes in a variety of print and electronic formats and by print-on-demand. Some material included with standard print versions of this book may not be included in e-books or in print-on-demand. If this book refers to media such as a CD or DVD that is not included in the version you purchased, you may download this material at http://booksupport.wiley.com. For more information about Wiley products, visit www.wiley.com.
国会图书馆控制编号:2014951018
Library of Congress Control Number: 2014951018
商标: Wiley、Wiley 徽标、Wrox、Wrox 徽标、Programmer to Programmer 以及相关商业外观均为 John Wiley & Sons, Inc. 及其附属公司在美国和其他国家/地区的商标或注册商标,未经书面许可不得使用。所有其他商标均为其各自所有者的财产。John Wiley & Sons, Inc. 与本书中提及的任何产品或供应商均无关联。
Trademarks: Wiley, the Wiley logo, Wrox, the Wrox logo, Programmer to Programmer, and related trade dress are trademarks or registered trademarks of John Wiley & Sons, Inc. and/or its affiliates, in the United States and other countries, and may not be used without written permission. All other trademarks are the property of their respective owners. John Wiley & Sons, Inc., is not associated with any product or vendor mentioned in this book.
献给我亲爱的朋友,Primrose 和 Albert。
For my darling buds, Primrose and Albert.
—斯科特·米利特
—SCOTT MILLETT
SCOTT MILLETT 是 Iglu.com 的 IT 总监,自 .NET 1.0 版本以来一直致力于此。他曾于 2010 年和 2011 年荣获 ASP.NET MVP 称号。他也是《专业 ASP.NET 设计模式》和《专业企业 .NET》的作者。如果您想就 DDD 或在 Iglu 的工作事宜联系 Scott,请发送邮件至scott@elbandit.co.uk ,或在 Twitter 上关注他@ScottMillett ,或通过https://www.linkedin.com/in/scottmillett成为他的好友。
SCOTT MILLETT is the Director of IT for Iglu.com and has been working with .NET since version 1.0. He was awarded the ASP.NET MVP in 2010 and 2011. He is also the author of Professional ASP.NET Design Patterns and Professional Enterprise .NET. If you would like to contact Scott about DDD or working at Iglu, feel free to write to him at scott@elbandit.co.uk, by giving him a tweet @ScottMillett, or becoming friends via https://www.linkedin.com/in/scottmillett.
NICK TUNE 热衷于解决业务问题、打造雄心勃勃的产品并不断学习。成为一名软件开发者确实是他的梦想职业。迄今为止,他职业生涯的亮点是在 7digital 工作,在那里他加入了一个自组织、以业务为中心的团队,每天最多部署 25 次生产环境。他未来的目标是与充满热情的同事一起开发激动人心的新产品,并不断成为一名更全面的问题解决者。
NICK TUNE is passionate about solving business problems, building ambitious products, and constantly learning. Being a software developer really is his dream job. His career highlight so far was working at 7digital, where he was part of self-organizing, business-focused teams that deployed to production up to 25 times per day. His future ambitions are to work on exciting new products, with passionate people, and continually become a more complete problem solver.
您可以在他的网站( www.ntcoding.co.uk)和 Twitter(@ntcoding )上了解有关 Nick 的更多信息以及他对软件开发、软件交付的看法以及他最喜欢的技术。
You can learn more about Nick and his views on software development, software delivery, and his favorite technologies on his website (www.ntcoding.co.uk) and Twitter (@ntcoding).
ANTONY DENYER 是一位开发人员、顾问和教练,自 2004 年以来一直从事专业软件开发工作。他参与过多个项目,有效地运用了 DDD 的概念和实践。最近,他一直倡导在自己的大多数项目中使用 CQRS 和 REST。您可以通过antonydenyer.co.uk发送电子邮件联系他,他的推特账号是@tonydenyer。
ANTONY DENYER works as a developer, consultant, and coach and has been developing software professionally since 2004. He has worked on various projects that have effectively used DDD concepts and practices. More recently, he has been advocating the use of CQRS and REST in the majority of his projects. You can reach him via e-mail at antonydenyer.co.uk, and he tweets from @tonydenyer.
项目编辑器
PROJECT EDITOR
罗斯玛丽·格雷厄姆
Rosemarie Graham
技术编辑
TECHNICAL EDITOR
安东尼·丹耶尔
Antony Denyer
制作编辑
PRODUCTION EDITOR
克里斯汀·奥康纳
Christine O'Connor
文字编辑
COPY EDITOR
凯伦·吉尔
Karen Gill
内容开发和汇编经理
MANAGER OF CONTENT DEVELOPMENT AND ASSEMBLY
玛丽·贝丝·韦克菲尔德
Mary Beth Wakefield
营销总监
MARKETING DIRECTOR
大卫·梅休
David Mayhew
营销经理
MARKETING MANAGER
凯莉·谢里尔
Carrie Sherrill
专业技术与战略总监
PROFESSIONAL TECHNOLOGY & STRATEGY DIRECTOR
巴里·普鲁埃特
Barry Pruett
业务经理
BUSINESS MANAGER
艾米·克尼斯
Amy Knies
联合出版商
ASSOCIATE PUBLISHER
吉姆·米纳特尔
Jim Minatel
项目协调员,封面
PROJECT COORDINATOR, COVER
布伦特·萨维奇
Brent Savage
校对
PROOFREADER
Jenn Bennett,Word One
Jenn Bennett, Word One
索引器
INDEXER
约翰娜·范胡斯·丁斯
Johnna VanHoose Dinse
封面设计师
COVER DESIGNER
威利
Wiley
封面图片
COVER IMAGE
首先,我要衷心感谢 Nick Tune 同意协助我完成这个项目,并为本书的许多章节做出了巨大贡献。我还要感谢 Rosemarie Graham、Jim Minatel 以及 Wrox 的所有同事,他们为本书的创作提供了帮助。还要感谢 Antony Denyer 作为技术编辑的出色工作。最后,非常感谢 Isabel Mack 提供的语法指导以及对 Leanpub 初稿的早期反馈。
FIRSTLY I WOULD LIKE to give a massive thanks to Nick Tune for agreeing to help me out with this project and contributing greatly to many of the chapters. I would also like to thank Rosemarie Graham, Jim Minatel, and all those at Wrox who have helped to create this book. Thanks as well to Antony Denyer who did a sterling job as the technical editor. Lastly, many thanks to Isabel Mack for the grammar pointers and early feedback of the Leanpub draft.
请访问www.wiley.com/go/eula访问 Wiley 的电子书 EULA。
Go to www.wiley.com/go/eula to access Wiley's ebook EULA.